본문 바로가기

Programming/Java & JSP & Spring

JVM Memory와 Garbage Collection

JVM Memory에 대해 알아보기 전에 JVM에 대한 구조와 개념을 살펴보자

 

프로그램이 실행되기 위해서는 Windows나 Linux같은 운영체제가 제어하고 있는 시스템 일부인 메모리(RAM)를 제어할 수 있어야 하는데, Java 이전에 C같은 대부분의 언어로 만들어진 프로그램은 이러한 이유 때문에 OS에 종속되어 실행되게 되었다.

 

Java로 만들어진 프로그램은 JVM이라는 프로그램만 있으면 실행이 가능한데, JVM이 OS에게서 메모리 사용 권한을 할당받고 JVM이 Java 프로그램을 호출하여 실행한다.

 

이렇게 JVM위에서 동작할 수 있는건 JVM이 Java Byte Code를 OS에 맞게 해석해주는 역할을 해주기 때문이다.

하지만 JVM의 해석을 거치기 때문에 C언어 같은 네이티브 언어에비해 속도가 느렸지만 JIT(Just In Time)컴파일러를 구현해 이점을 극복했다.

  • Java와 OS사이에서 중계자 역할
  • Java가 OS에 구애받지 않게 해줌
  • 메모리 관리 기능 (Garbage Collection)

JVM 구조

프로그램이 실행되면 JVM은 OS로부터 이 프로그램이 필요로 하는 메모리를 할당받고, JVM은 이 메모리를 용도에 따라 여러 영역으로 나누어 관리한다.

 

Class Loader
JVM내로 class파일을 로드하고, 링크를 통해 배치하는 작업을 수행하는 모듈이다. jar파일 내 저장된 클래스들을 JVM위에 탑재하고 사용하지 않는 클래스들을 메모리에서 삭제한다.

 

Execution Engine
클래스를 실행시키는 역할이다. 클래스 로더가 JVM내의 런타임 데이터 영역에 바이트코드를 배치시키고, 이것은 실행엔진에 의해 실행된다.

 

Interpreter
Execution Engine은 자바 바이트 코드를 명령어 단위로 읽어서 실행한다. 한줄씩 읽어서 수행하기 때문에 느리다.

 

JIT (Just - In - Time)
인터프리터 방식의 단점을 보완하기 위해 도입된 JIT컴파일러이다.
인터프리터 방식으로 실행하다가 적절한 시점에 바이트코드 전체를 컴파일하여 네이티브 코드로 변경한다.

 

Garbage collector
GC를 수행하는 모듈이다.

Java 어플리케이션 실행과정

  1. 프로그램이 실행되면 JVM은 OS로부터 메모리를 할당받는다.
  2. 자바 컴파일러(javac)가 자바 소스코드(.java)를 읽어들여 자바 바이트코드(.class)로 변환시킨다.
  3. Class Loader를 통해 class파일들을 JVM으로 로딩한다.
  4. 로딩된 class 파일들은 Execution engine을 통해 해석된다.
  5. 해석된 바이트코드는 Runtime Data Area에 배치되어 실질적인 수행이 이루어지게 된다.
  6. 이러한 실행과정 속에서 JVM은 필요에 따라 GC같은 관리작업을 수행한다.

Runtime Data Area

프로그램을 수행하기 위해 OS에 할당받은 메모리 공간

PC Register

Thread가 시작될 때 생성될때마다 생성되며 스레드마다 하나씩 존재한다. Thread가 어떤 부분을 어떤 명령으로 실행해야할 지에 대한 기록을 하는 부분으로 현재 수행중인 JVM명령의 주소를 갖는다.

JVM 스택 영역

프로그램 실행과정에서 임시로 할당되었다가 메소드를 빠져나가면 바로 소멸되는 특성의 데이터를 저장하기 위한 영역이다.

메소드 호출 시마다 공간이 생성되고 메소드가 끝나면 삭제한다. 메소드 안에서 사용되는 로컬변수나 매개변수 리턴 값 및 연산에 일어나는 값들을 임시적으로 저장한다.

Native Method Stack

자바가 아닌 다른 언어로 작성된 코드를 위한 공간이다.

Method Area (= Class Area = Static Area)

클래스 정보를 처음 메모리 공간에 올릴 때 초기화되는 대상을 저장하기 위한 메모리 공간이다.

사실 상 컴파일된 거의 모든 바이트코드가 여기에 올라간다봐도 무방하다.

Heap

객체를 저장하는 가상 메모리 공간이다.

new 연산자로 생성된 객체와 배열을 저장한다. 물론 Method Area영역에 올라온 클래스들만 객체로 생성할 수 있다.

 

Method Area가 클래스 데이터를 위한 공간이라면 Heap영역이 객체를 위한 공간이다.

Heap과 MethodArea는 GC 관리대상에 포함된다.

 

Heap 영역은 세 부분으로 나눌 수 있다.

1. Permanent Generation

2. New / Young

3. Old

 

1. Permanent Generation

JVM기동시 사용되는 모든 Class, Method의 Meta정보가 포함된다. 또한 상수화된 String도 여기 포함된다.

또한 Reflection을 사용하여 동적으로 클래스가 로딩되는 경우에 사용된다. 내부적으로 Reflection 기능을 자주 사용할 경우 이 영역에 대한 고려가 필요하다.

Metaspace(Java 8 이후)

Permanent Generation이 Metaspace로 변경 되었다.

기능은 비슷하지만, 주요 차이점은 동적으로 사이즈를 바꿀 수 있다.

JVM 옵션도 PermGem관련하여 사라지고, Metaspace 관련하여 MetaspaceSize 및 MaxMEtaspaceSize 옵션이 새로 생겼다.

아래에서 좀 더 알아보자.

 

2. New / Young

  • Eden : 객체들이 최초로 생성되는 공간
  • Survivor 0/1 : Eden에서 참조되는 객체들이 저장되는 공간

3. Old

New영역에 저장되었던 객체 중에 오래된 객체가 이동되어 저장되는 공간

PermGen과 Method Area

위의 설명에 따르면 두 영역 다 Java Application에 구동되는 클래스와 메소드들이 올라간다. 무슨 차이인가?

Method Area 는 JVM 제품(벤더)에 따라 구현이 다르다.

Hotspot JVM(Oracle)의 Method Area 는 Permanent Area(PermGen)이라고 한다.

Metaspace??

Perm Gen에 대한 많은 이슈가 있었기 때문에 Java8에서는 사라지고 아래로 대처되었다.

  1. Class 의 Meta정보 → Metaspace 영역으로 이동
  2. Method의 Meta 정보 → Metaspace 영역으로 이동
  3. Static Object → Heap 영역으로 이동
  4. 상수화된 String Obejct → Heap 영역으로 이동
  5. 클레스와 관련된 배열 객체 Meta 정보 → Metaspace로 간듯
  6. JVM 내부적인 객체들과 최적화컴파일러(JIT)의 최적화 정보 → Metaspace로 간듯

Java7까지는 Heap 영역이 New, Old, Perm 였다면

Java8에서는 New, Old로 아키텍쳐가 변경되었다.

즉, MetaSpace는 Heap영역의 부분이 아니게 된 것이다. 그러므로 MetaSpace의 최대 메모리는 이제 Heap영역이 아닌 OS메모리까지 할 수 있게 된 것이다.

-XX:MetaspaceSize -XX:MaxMetaspaceSize 로 영역 사이즈를 조정할 수 있다.

GC 동작 방식

위에서도 살펴봤지만 새로 생긴 객체는 Young 오래된 객체는 Old에 있게된다.

New안에서의 Eden영역은 객체가 생성되자마자 저장되는 곳이다. 이렇게 생성된 객체는 Minor GC가 일어나면 Survivor 영역으로 이동한다.

이 Survivor 영역은 또 1, 2로 나눠지는데 Minor GC가 발생하면 alive한 객체는 Survivor2로 이동하고 Survivor1과 Eden을 Clear하는 것이다.

다음 번 Minor GC때는 Alive한 객체가 Survivor1으로 이동하고 2와 Eden을 클리어한다.

이렇게 수행하다가 Survivor에서 오래된 객체는 Old 영역으로 넘어간다.

Old영역에서 일어나는 GC를 Full GC라고 부르며 속도가 매우 느리다.

왜냐하면 전체 객체의 reference를 쭉 따라가다가 참조가 되어있지 않은 쓸모없는 객체라면 삭제하기 때문이다.

GC가 왜 중요한가?

Minor GC의 경우 보통 0.5초 내에 끝나기 떄문에 큰 문제가 되지 않는다.

그러나 Full GC의 경우 보통 수초가 수용되고 그 동안에 Java Application이 멈춰버리기 때문에 문제가 될 수 있다.

물론 멈추는 동안 요청을 Queue에 저장되었다가 작동하지만, 그 요청이 한꺼번에 몰리게되면 과부하에 의한 여러 장애를 만들 수 있다.

메모리 튜닝 옵션

전체 Heap Size 조정 옵션

전체 Heap Size는 -Xms와 -Xmx 로 Heap사이즈의 영역을 조정할 수 있다.

예를 들어 -Xms512m -Xmx1024m 으로 설정하면 JVM은 전체 Heap Size를 Application 상황에 따라서 512MB ~ 1024MB 사이에서 사용하게 된다.

하지만 heap을 줄이고 늘리는데 리소스가 소모되므로 메모리 변화량이 큰 애플리케이션이 아니라면 Min과 Max 사이즈를 동일하게 설정하는 것이 좋다.

PermGen Size 조정 옵션

Perm Size는 위에서도 말했듯이 Java Application의 모든 클래스와 메소드 정보들이 올라간다.

많은 수의 클래스가 올라가는 어플리케이션의 경우 WAS가동 시에 Out of Memory가 발생할 수 있다.

Perm Size는 -XX:PermSize= 으로 최초 PermGen사이즈를 조정, -XX:MaxPermSize= 옵션으로 최대 사이즈를 조정할 수 있다. 일반적으로 WAS에서 Perm Size는 64 ~ 256MB가 적절하다.

New 영역과 Old 영역의 조정 옵션

-XX:NewRatio= 옵션으로 비율을 조정할 수 있다. 전체 HeapSize가 768MB이고 -XX:NewRatio=2 일 때, New영역이 256MB, Old영역이 512MB로 설정된다.

-XX:NewSize= 로 직접 New영역의 사이즈를 조정할 수도 있다.

보통 서버 Application의 경우 New 1 : Old 2 로 잡고 클라이언트 Application의 경우 New 1 : Old 3~5 정도로 잡는다.

Survivor 영역 조정 옵션

-XX:SurvivorRatio= 옵션으로 Suvivor 영역의 비율을 조정할 수 있다. 만약 ratio가 64MB라면 EDEN영역이 128MB, Survivor영역은 2MB가 된다.

-server와 -client 옵션

-server 옵션은 server용 application에 최적화된 옵션이다. 서버 특성 상 빠른 response time을 내는데 집중이 되어있다.

또한 특정 사용자가 서버를 계속 사용하는 것이 아니기 때문에 사용자에 관련된 객체들이 오래 지속될 경우가 드물다.

그래서 상대적으로 Old영역이 작고 New영역이 크게 배정된다.

-client 옵션은 워드프로세서처럼 혼자 사용하는 application이다. 그래서 client application은 response time보다 빨리 기동되는데 최적화 되어있다.

application이 종료될 때까지 남아있는 객체 비중이 높기 때문에 상대적으로 Old영역의 비율이 높다.

참고링크

https://5dol.tistory.com/183

https://epthffh.tistory.com/entry/JVM-메모리-관련-설정

http://karunsubramanian.com/websphere/one-important-change-in-memory-management-in-java-8/