Home 가비지 컬렉터 튜닝
Post
Cancel
java gc

가비지 컬렉터 튜닝

지난 이야기

Garbage Collector에 대해 정리해놓은 글에서 Reference & Reachable Object와 Heap 영역의 구조, GC 알고리즘에 대해 설명했습니다. 이번 시간에는 Parallel Collector에 대해 조금 더 설명한 후에 GC 튜닝 방법에 대해 설명하려고 합니다.

Parallel Collector (Throughput Collector)

Java8에서 default collector입니다. Serial Collector와 비슷하게 동작하지만 차이점은 여러 개의 쓰레드를 가지고 Garbage Collect를 수행해 더 빠르게 동작할 수 있습니다. 멀티 코어나 프로세서환경에서는 이점이 있지만 싱글 코어이거나 힙 사이즈가 100MB 이하인 경우 Serial Collector와 별다른 차이가 없을 수 있습니다. 각 실행환경마다 다르니 많은 테스트를 통해 환경에 맞는 Collector와 파라미터를 찾아야 합니다.

1
 -XX:+UseParallelGC

위 옵션으로 사용할 수 있습니다.

Parallel Collector에서 사용할 수 있는 여러 옵션이 있습니다. 첫번째로 Garbage Collect에 사용할 쓰래드 수를 결정할 수 있습니다.

1
-XX:ParallelGCThreads=<N>

항상 N만큼 쓰레드 수를 Garbage Collect에 사용하는 것은 아니고 일정 비율을 곱해 나온 수만큼 사용합니다. 예를 들어 N이 꽤 클 경우(8이상)에는 5/8을 곱합니다.

또한 옵션으로 행위 기반 튜닝을 할 수 있습니다. 원하는 목표치를 JVM에게 힌트로 주고 JVM은 내부적으로 통계치를 이용하거나 다른 변수들을 조절해 목표치를 달성합니다. 아래 3가지 목표를 설정할 수 있습니다.

  • 최대 일시 정지 시간 최소화 목표 -XX:MaxGCPauseMillis=<nnn>
    • 너무 낮게 설정하면 전체적인 처리율이 떨어짐
    • GC가 일시 정지 시간을 예측해서 그에 맞게 Heap 사이즈나 여러 변수들을 조정함
  • 처리율 향상 목표 -XX:GCTimeRatio=<nnn>
    • GC시간과 애플리케이션 시간 비율이 $1\over1+nnn$으로 설정
  • Footprint

    1
    2
    
      -Xms=<nnn>
      -Xms=<mmm>
    
    • 최대 일시 정지 시간, 처리율 향상 목표를 달성한다면 GC는 두 목표중 하나가 실패할 때 까지 Heap사이즈를 줄임
    • 일시 정지 시간 최소화 목표에 의해 Heap 사이즈는 계속 커진 다음에 최대 처리율을 달성하기 위해 계속 Heap사이즈는 줄어듬
    • 이렇게 경쟁적인 목표를 계속 달성하려고 하면서 최적의 값을 찾음

Garbage Collector 튜닝이 필요한 이유

모든 프로젝트에 진행할 필요는 없습니다. 하지만 힙 사이즈를 설정했거나 Timeout과 같은 에러메시지가 로그에서 나온다면 튜닝이 필요하다고 할 수 있습니다. 프로그램이 생각보다 많은 메모리를 필요로 하다면 팀의 코딩 스타일을 살펴볼 필요가 있습니다. StringBuffer나 StringBuilder를 활용하지 않고 String으로만 사용한다던지, 매우 큰 용량의 파일을 열어놓고 닫지는 않는다든지 하는 프로그램 로직적인 부분을 먼저 살펴보고 튜닝을 진행해야 합니다. 먼저 말씀드리지만 GC 튜닝은 실험적인 방법으로 최적화를 합니다. 다른 프로젝트에서 좋은 성능을 보였던 옵션이 이번 프로젝트에서는 오히려 낮은 성능을 초래하고 심지어 OutOfMemory 예외를 만날 수도 있습니다.

먼저 trade-off 관계인 아래 지표를 봅시다

  • 최대 정지 시간
  • 총 처리율

두 지표 모두 힙 사이즈에 큰 영향을 받고 대부분의 상황에서 비례 관계를 갖습니다. 따라서 적당한 값을 어플리케이션의 특성에 맞게 조절해야합니다. 최대 정지 시간을 너무 높게 잡으면 사용자들이 보낸 요청을 STW가 일어나는 동안은 처리할 수 없고 이는 장애로 인식될 수 있습니다. 그렇다고 최대 정지 시간을 낮추려고 힙사이즈를 줄이면 GC가 매우 빈번히 일어날 것이고 Context Switch 비용 때문에 총 처리율이 낮아질 수 있습니다. 이는 최대 동시 접속자 수를 낮추게 될 것이고 원하는 최대 동시 접속자 수만큼 수용이 가능한지 체크해봐야 합니다.

Garbage Collector 모니터링 방법

jstat

jstat은 HotSpot JVM에 있는 모니터링 도구이다. jdk를 설치하면 $JAVA_HOME/bin 디렉토리에 포함되어 있어 jstat으로 바로 실행할 수 있습니다. 각 힙 영역의 최대, 최소 사이즈와 현재 사이즈를 볼 수 있고, 지난 GC의 원인 및 GC 수행 시간을 확인할 수 있습니다. -gcutil(또는 -gccause), -gc, -gccapacity을 가장 많이 사용합니다. 각 컬럼의 뜻은 아래와 같습니다.

칼럼 설명 jstat 옵션

칼럼설명jstat 옵션
S0CSurvivor 0 영역의 현재 크기를 KB 단위로 표시-gc -gccapacity -gcnew -gcnewcapacity
S1CSurvivor 1 영역의 현재 크기를 KB 단위로 표시-gc -gccapacity -gcnew -gcnewcapacity
S0USurvivor 0 영역의 현재 사용량을 KB 단위로 표시-gc -gcnew
S1USurvivor 1 영역의 현재 사용량을 KB 단위로 표시-gc -gcnew
ECEden 영역의 현재 크기를 KB 단위로 표시-gc -gccapacity -gcnew -gcnewcapacity
EUEden 영역의 현재 사용량을KB 단위로 표시-gc -gcnew
OCOld 영역의 현재 크기를 KB 단위로 표시-gc -gccapacity -gcold -gcoldcapacity
OUOld 영역의 현재 사용량을KB 단위로 표시-gc -gcold
PCPermanent영역의 현재 크기를 KB 단위로 표시-gc -gccapacity -gcold -gcoldcapacity -gcpermcapacity
PUPermanent영역의 현재 사용량을KB 단위로 표시-gc -gcold
YGCYoung Generation의 GC 이벤트 발생 횟수-gc -gccapacity -gcnew -gcnewcapacity -gcold -gcoldcapacity -gcpermcapacity -gcutil -gccause
YGCTYong Generation의 GC 수행 누적 시간-gc -gcnew -gcutil -gccause
FGCFull GC 이벤트가 발생한 횟수-gc -gccapacity -gcnew -gcnewcapacity -gcold -gcoldcapacity -gcpermcapacity -gcutil -gccause
FGCTFull GC 수행 누적 시간-gc -gcold -gcoldcapacity -gcpermcapacity -gcutil -gccause
GCT전체 GC 수행 누적 시간-gc -gcold -gcoldcapacity -gcpermcapacity -gcutil -gccause
NGCMNNew Generation의 최소 크기를 KB단위로 표시-gccapacity -gcnewcapacity
NGCMXNew Generation의 최대 크기를 KB단위로 표시-gccapacity -gcnewcapacity
NGCNew Generation의 현재 크기를 KB단위로 표시-gccapacity -gcnewcapacity
OGCMNOld Generation의 최소 크기를 KB단위로 표시-gccapacity -gcoldcapacity
OGCMXOld Generation의 최대 크기를 KB단위로 표시-gccapacity -gcoldcapacity
OGCOld Generation의 현재 크기를 KB단위로 표시-gccapacity -gcoldcapacity
PGCMNPermanent Generation의 최소 크기를 KB단위로 표시-gccapacity -gcpermcapacity
PGCMXPermanent Generation의 최대 크기를 KB단위로 표시-gccapacity -gcpermcapacity
PGC현재 Permanent Generation의 크기를 KB단위로 표시-gccapacity -gcpermcapacity
PCPermanent 영역의 현재 크기를 KB단위로 표시-gccapacity -gcpermcapacity
PUPermanent 영역의 현재 사용량을 KB단위로 표시-gc -gcold
LGCC지난 GC의 발생 이유-gccause
GCC현재 GC의 발생 이유-gccause
TTTenuring threshold. Young 영역 내에서
이 횟수만큼 복사되었을 경우(S0 ->S1, S1->S0) Old 영역으로 이동
-gcnew
MTT최대 Tenuring threshold. Yong 영역 내에서
이 횟수만큼 복사되었을 경우 Old 영역으로 이동
-gcnew
DSS적절한 Survivor 영역의 크기를 KB단위로 표시-gcnew

위 정보를 jstatd를 통해 원격에서도 볼 수 있고 visual GC를 통해 GUI환경에서 조금 더 직관적이고 쉽게 볼 수 있습니다.

JVM Visual

jstat은 JVM을 실행할 때 별다른 옵션을 주지않아도 되는 장점이 있지만 GC 수행에 대해서는 간단한 정보만 제공하는 단점이 있습니다. 각 GC의 개별적인 수행 정보를 알고 싶으면 JVM을 실행할 때 -verbosegc 옵션을 주어 얻을 수 있습니다. 이 정보에서 Full GC와 Minor GC가 몇 번 일어났고 수행 전 후의 메모리 사이즈의 변화도 알 수 있습니다. 시각화 툴로는 HPJmeter를 사용할 수 있습니다.

튜닝

튜닝 절차

1. GC 모니터링 결과 분석 후 GC 튜닝 여부 결정

jstat이나 jvm의 -verbosegc 설정으로 GC 모니터링을 합니다. GC 모니터링 결과 Full GC가 1초가 넘어가거나 너무 자주 발생한다면 GC 튜닝을 진행해야 합니다. 고려해야하는 것은 young영역과 old영역의 비율, Heap 사이즈, Survivor와 Eden의 비율, Survivor의 Max aging 등이 있습니다.

2. GC 방식/메모리 크기 지정 등 파라미터 변경

여러 GC 중 적합한 방식의 GC를 선택해서 적용해야 하고 메모리 크기와 비율도 어플리케이션의 특성에 맞춰 변경해야 합니다. 하지만 예상과 다른 전혀 다른 GC와 파라미터가 성능이 좋을 수도 있습니다. GC 튜닝은 실험과학이라는 사실을 기억해야 합니다. 여러 서버에 다양한 파라미터를 적용하고 충분한 시간을 기다려야 합니다.

3. 결과가 만족스러울 경우 전체 서버에 반영 및 종료

OutOfMemory가 발생하지 않고 성공적으로 원하는 GC 시간과 빈도를 달성했다면 전체 서버에 반영하고 예후를 지켜봅니다.

튜닝시 주의할 점

튜닝을 하기 전에 메모리 누수나 메모리를 남용하고 있는 코드가 없는지 살펴봐야 합니다. 힙 메모리를 덤프한 후 튜닝 전략을 세우는 것도 좋지만 특정 상황에 치우친 튜닝을 하기 쉽고 서비스의 사용량이 많고 사용자의 패턴이 아주 다양한 경우에는 OutOfMemory 에러나 오히려 GC 소요시간이 길어지거나 빈도가 증가할 수 있습니다. 따라서 실험적인 방법을 통해 경험을 축적하면서 자연 선택적으로(?) 튜닝을 하는 방법을 추천한다고 합니다.

출처

이 기사는 저작권자의 CC BY 4.0 라이센스를 따릅니다.

리눅스 헷갈리는 개념 포스팅

Spring Boot와 @WebMVCTest로 MVC Controller를 테스트 하는 방법