Parallel GC Tuning
Parallel gc는 java에 대표적인 처리량 throughtput 중심의 gc다, 즉 개별 중단 시간을 줄이는 것 보단, 애플리케이션이 실질적으로 작업을 수행하는 총 시간을 늘리는데, 최적화 되어있다.
parallel gc의 튜닝은 주로 목표 기반 goal-oriented 방식으로 접근해야 한다.
즉, 세부적인 메모리 사이즈를 직접 지정하기보다 jvm에게 최대 멈춤 시간과 처리량 목표를 제시하면 jvm이 이에 맞춰 heap 크기를 동적으로 조절하게 하는것이 효과적이다.
약간 IHOP를 Adaptive하게 조정하는 G1GC의 기능하고 비슷하다면 비슷한 튜닝 메커니즘이라고 보이기도한다.
목표 설정 #
parallel gc는 인체공학적(Ergonomics)이라고 불리는 자동 조절 기능이 매우 강력하다.
따라서 다음 우선 순위에 따라 jvm에게 힌트를 주는 순서로 진행된다.
- max pause time: gc로 인해 멈추는 시간의 한계치
- throughput: 전체 실행 시간중 gc 시간을 제외한 애플리케이션의 실행 시간 비율
- footprint: 힙 메모리 사용량 최소화
jvm은 1을 만족시키려고 노력하고 그게 되면 2, 다음은 3을 만족시키려 한다.
flags #
일단 위에서 알아본 3가지 순서대로 주요플래그를 알아보겠다
최대 멈춤 시간 목표 설정 #
가장 먼저 설정해야할 소프트 제한인데, jvm은 이 시간 안에 gc를 끝내기 위해 young/old generation의 크기를 자동으로 줄일 수 있다.
-XX:MaxGCPauseMillis=<N>- gc가 발생했을때 N 밀리초 이상 멈추지 않도록 노력하는 설정
- 이 값을 너무 낮게 잡으면, jvm이 목표 달성을 위해 힙 크기를 줄이고 결과적으로 gc가 너무 자주 발생해 전체적인 처리량이 급격하게 떨어질 수 있다.
https://docs.oracle.com/javase/8/docs/technotes/guides/vm/gctuning/parallel.html
g1gc에서의 튜닝포인트도 이걸 튜닝하지 않는가? 최대 퍼즈타임을 설정하기 위해 g1에서도 해당 값을 지정한다.
g1gc에서는 치울 구역 즉 region을 골라내 목표 시간내에 처리 가능한 Region 개수만큼 청소하고 목표시간을 짧게 잡으면 old영역 청소를 미루다가 나중에 fullgc가 터질수있는 위험이있는, 디폴트도 200ms로 되어있는 옵션이고
parallel gc에서 해당 옵션은 세대 크기를 조절하기위해 전체 young old 영역을 리사이징 하여 시간을 맞추고 목표 시간을 짧게 잡으면 힙이 쪽르아들어 gc가 미친듯이 자주 발생한다. 기본값은 무제한이며 제약조건은 1순위
parallel은 리전별로 관리하는게 아니라 힙 전체를 해당 퍼즈타임내로 끊으려고 조절하므로 즉 힙사이즈가 크면 청소할 영역이 많아지니까 그걸 줄어버리자는 방식이고 g1은 해당하는 리전만 골라내는거지 영역 자체를 줄이지 않으니까 다르다고 볼수있다 오히려 parallel gc의 해당 시간은 넉넉하게 안주면 힙이 작아진다.
g1하고 다른 점을 정리하면 g1은 해당 플래그 값 내로 청소할 리전수를 고르고 parallel은 해당 시간을 만족하기 위해 힙을 줄여버린다. 동작방식이 다름. 그래서 g1은 너무 줄이면 heap이 작아져서 안댐
처리량 목표 설정 #
Parallel GC의 존재 이유다. 전체 시간중 gc가 차지하는 시간의 비율을 설정한다.
- 옵션:
-XX:GCTimeRatio=<N> - 공식: GC의 시간 비율은
1 / (1 + N)이다.- 기본값은 99다 즉 1/100 1%가 전체 시간중 gc 비율인거다.
- 만약 N=19로 설정하면 1/20 = 5%가 되는것이다.
MaxGCPauseMillis를 만족시킨 상태에서 이 목표를 달성하지 못하면, jvm은 old gen을 늘려 gc 빈도를 줄이려고 시도한다.
힙 메모리 크기 설정 #
위 두 목표만으로 해결되지 않을 때, 물리적인 메모리 한계를 지정한다.
-Xms<size>: 초기 힙크기 가급적으로 -Xmx와 동일하게 설정하면 동적 크기 변경 오버헤드를 줄일수있으니 좋다.-XmX<size>: 최대 힙크기
단계별 튜닝 프로세스 #
튜닝은 한 번에 모든 플래그를 넣는것이 아니라 관찰하며 조절해야한다.
일단 모니터링 환경을 구축해야하는데 튜닝 전에 현재 상태를 알아야하기 때문이다 gc로그를 활성화하자.
-Xlog:gc*(Java 9 이상), -XX:+PrintGCDetails -XX:+PrintGCTimeStamps (Java 8 이하)
2단계 메모리 할당: 애플리케이션이 OutOfMemoryError 없이 돌아갈 수 있는 적절한 힙 크기를 설정한다, 보통 필요한 live data 크기의 3~4배 정도를 권장한다.
3단계: 멈춤 시간 목표 설정 (MaxGCPauseMillis) 애플리케이션의 SLA에 맞춰 설정한다.
4단계: 처리량 목표 조절: GCTimeRatio 만약 멈춤 시간은 만족하는데, gc가 너무 자주 돈다면, 처리량 목표 조정을 하거나 힙크기를 늘려야한다.
Generation 비율 조정 #
자동 튜닝이 의도대로 동작하지 않을 때만 수동으로 Generation 비율을 건드린다.
-XX:NewRatio=<N>: Young Gen, Old Gen의 비율(기본값 2)- N=2일때 young:old는 1:2이다.
- 객체 생성과 소멸이 매우 빈번하다면 young gen을 늘리는것이 유리할 수 있다.
-XX:SurvivorRatio=<N>: Eden 영역과 Survivor 영역의 비율이다. 너무 작으면 서바이버 영역이 넘쳐나 객체가 곧바로 Old gen으로 promotion 될 수 있다.
parallel gc 튜닝은 처리량을 최우선으로 할 때 적합하고 짧은 응답속도가 중요하면 g1gc나 zgc로 전환하는걸 고려하자
힙크기변동: parallel gc는 목표 달성을 위해 힙 크기를 계속 변결할 수 있다. 운영 환경에서는 -Xms와 -Xmx를 동일하게 설정하여 힙 크기를 고정하는 것이 예측 가능성을 높이는데 좋다.