[JVM] 기본 GC 튜닝

2021. 12. 16. 14:52 JAVA/JVM

4개의 GC 알고리즘에 공통적으로 적용되는 기본적인 튜닝 방법에 대해 알아보자

Heap Size

힙이 너무 작다면 너무 자주 GC가 일어날 것이고,
힙이 너무 크다면 중단은 줄어들지만 Full GC가 발생했을 때 중단 시간이 너무 길어진다. 

OS 메모리와 힙 사이즈

힙 사이즈는 [물리적 RAM - (OS에서 사용하는 RAM)] 보다 작게! 
OS는 스와핑, Paging을 통해 메모리를 가상화시켜서 관리한다.
예를들어 OS는 실제로 PC의 RAM은 8GB이지만 가상화를 통해 16GB로 보이도록 제공한다.
이때, 실제로 물리적인 RAM은 8GB이기 때문에, 나머지 8GB를 디스크에서 가져와서 사용한다.
이 과정은 사용자에게는 투명하지만 내부적으로 디스크에 있는 데이터에 접근할때마다 느려질 것이다. 

힙 크기를 물리적인 RAM크기 보다 작게 지정하자. 일반적으로 보통 OS에서 프로파일을 위해서 적어도 1GB의 공간이 필요하기 때문에 RAM - 1GB의 크기로 힙사이즈를 지정하자. 

힙 크기를 물리적인 RAM 보다 크게 지정할 경우, Full GC가 발생한다면 디스크에 있는 가상 RAM에 접근해서 Full GC 를 처리해야하기 때문에 엄청 느려진다. 
디스크 가상화를 통한 메모리에 GC가 발생하여 디스크 접근이 발생할 수 있다는 것을 주의하자!

힙 크기 조절 [-XmsN ~ -XmxN]

명시하지 않으면 운영체제와 JVM에 따라 Default 값이 사용된다. 
초기 힙, 최대 힙 사이즈를 모두 지정할 경우 JVM은 GC가 너무 많이 발생한다면 최대 힙 사이즈가 될 때까지 힙을 지속적으로 늘린다. 
JVM이 알아서 명시한 사이즈 내에서 적합한 힙 크기를 찾으려 시도한다. 
큰 힙이 필요한 어플리케이션이 아니라면, 그냥 디폴트 값을 사용하고 힙 크기를 미세조정하는 시간에 다른 튜닝을 고려하자.

적절한 힙 사이즈를 찾는 공식은 없다. 어플리케이션이 돌아가는 것을 관찰하고 힙 사이즈를 조정해가면서 GC 가 일어나는 시간을 파악하고, 적절한 성능 목표치 내에서 타협해야 한다. 
어플리케이션을 관찰하고 적절한 힙 사이즈가 측정되었다면,
-Xms4096m -Xmx4096m 처럼 힙 크기를 고정할 수 있다. 
힙 크기를 고정하면 힙 크기 재조정 여부를 파악하는데 쓰이는 불필요한 작업들이 없어지기 때문에 GC 는 약간 더 효율적이 된다. 

제너레이션 크기 조절

지정된 힙 크기 내에서 영/올드 제너레이션의 크기를 지정해야 한다. 
영 제너레이션이 비교적 크다면 Minor GC가 덜 발생하고 더 작은 객체가 Old 제너레이션으로 간다. 하지만 비교적 올드 제너레이션이 더 작기 때문에 더 자주 Full GC가 발생한다. 이 균형을 설정하는 것이다. 

영 제너레이션을 설정하는 플래그만 제공한다.
나머지 공간은 모두 올드 제너레이션이 차지한다. 

-XX:NewRatio=N ( Default = 2)
올드 제너레이션과 영 제너레이션의 비율을 설정한다

-XX:NewSize=N
영 제너레이션의 초기 크기를 설정한다

-XX:MaxNewSize=N

영 제너레이션의 최대 크기를 설정한다

-XmnN
NewSize, MaxNewSize에 동일한 값 N을 설정한다

Default 크기
NewSize, MaxNewSize를 지정하지 않을 경우, 아래 공식에 따라 초기 영 제너레이션의 크기가 결정된다. 
초기 영 제너레이션의 크기 = 초기 힙 크기 / ( 1 + NewRatio )
NewRatio값은 디폴트가 2기때문에, 영 제너레이션은 초기 힙 크기의 33%가 된다. 


Permanent Generation (=메타 스페이스)

-XX:PermSize=N ~ -XX:MaxPermSize=N
-XX:MetaspaceSize=N ~-XX:MaxMetaspaceSize=N


힙 메모리는 영/올드 외에도 펌 제너레이션 영역이 있다. 
JVM이 로드할 클래스들의 메타데이터를 저장하는 공간이다. 
독립적인 힙처럼 동작한다. 
Java 7에서는 PERM이였던 것이 Java 8에서는 메타스페이스로 불린다.
이곳도 튜닝될 필요가 있다!

메타 스페이스가 가득차면?
OOME :Out Of Memory Error 발생!

Perm 에 저장된 클래스 정보 데이터는 영구적이지 않다.
개발중인 어플리케이션 서버에서는 펌 영역이 가득차고 기존 클레스 메타 데이터가 폐기될 때 가끔 Full GC 가 발생한다. 

힙 덤프를 통해 어떤 클래스 로더가 동작했는지 진단할 수 있다.
이를 통해 클로스로더에 누수가 있는지 알아낼 수 있다. 

-XX:ParallelGCThreads=N

시리얼 컬렉터를 제외한 모든 GC 알고리즘은 여러 개의 스레드를 사용한다. 
이 때 사용되는 스레드의 개수를 지정한다. 
단, CMS 나 G1에서 사용하는 백그라운드 스레드의 수를 설정하지는 않는다. 
스레드의 개수는 아래의 경우만 영향을 받는다

-XX:+UseParallelGC & Minor GC 일때
-XX:+UseParallelOldGC & Full GC 일때
-XX:+UseParNewGC & Minor GC
-XX:+UseG1GC & Minor GC
CMS의 stop-the-world 일때 ( Not Full GC)
G1의  stop-the-world 일때 ( Not Full GC)

머신 내의 CPU 개수를 기반으로 기본 스레드 개수가 지정된다
CPU 8개 이상을 갖는 머신에서의 기본 스레드 개수는 다음과 같다
ParallelGCThreads = 8 + ( (N-8) * 5 / 8)

자동 크기 조정

-XX:-UseAdaptiveSizePolicy (Default : true)
주어진 힙 크기 내에서 좋은 성능을 내도록 JVM이 자동으로 영/올드 비율을 튜닝한다. 
힙 안의 제너레이션의 크기는 JVM이 최적의 성능을 찾고자 시도하면서 실행하는 동안 달라질 수 있다. 
힙 내에서 영 제너레이션과 올드 제너레이션의 비율을 자동으로 바꿔준다. 
대게 GC로 인해 중단이 일어나는 동안 크기 조정이 일어난다. 
플래그 이용해서 끄지 않더라도, 최대, 최소 크기를 갖게 설정할 경우 자동 크기 조정이 동작하지 못한다. 
-XX:+PrintAdaptiveSizePolicy
JVM이 어플리케이션 내의 공간 크기를 조절하는 방법을 출력해준다
GC 가 수행되면서 크기가 조정되는 것을 볼 수 있다 

GC 모니터링 도구

힙 덤프 분석
GC 로그 남기기
-XX:+PrintGCDetails (권장)
-XX:+PrintGCTimeStamps
-XX:+PrintGCDateStamps
GC Histogram

힙 실시간 분석
jconsole
$ jstat -gcutil [process_id] 1000
==> 1000ms 마다 process_id의 힙 메모리 크기, YGC, FGC의 횟수를 출력한다
$ jmap -heap [process_id]
==> process_id의 현재 힙 정보를 출력

모니터링 도구는 그때그때 필요하면 찾아쓰면되고...
중요한 것은 사용하는 GC 알고리즘이 출력하는 로그를 분석할 줄 아는 능력이다.
다음부터 GC 알고리즘 하나하나 로그를 보는 법을 살펴보자!