Gabage Collection

Heap 구조, GC의 기본동작, 오라클 JCM에서 지원하는 GC 알고리즘에 대해 알아보자 ~

1. Oracle 문서에 권하는 기본 GC 튜닝 전략

  • swap 메모리 사용을 피함
  • 기본값보다 더 큰 힙이 필요하지 않는 이상 힙 크기를 변경하지 않음
    • 단, 물리 메모리보다 더 크게 힙을 설정하면 안됨
  • 튜닝이 필요하다면 Behavior based tuning을 시도

  • 두 가지 옵션을 지정하여 자동으로 GC를 튜닝하는 방법
    1. 최대 중지 시간 목표 (Max pause time goal)
      • GC가 프로그램 쓰레드를 멈추는 시간의 최대값
    2. 성능 목표 (Throughput goal)
      • 프로그램 쓰레드 대비 GC가 실행되는 시간의 비율
    3. 메모리 사용량 최적화 (Footprint goal)
      • 위 두 옵션을 만족했을 때 자동으로 메모리 사용량을 줄여나가는 기능
      • 따로 설정하지 않음
  1. 최대 중지 시간 목표 (Max pause time goal)
    • -XX:MaxGCPauseMillis=
    • 기본값은 무제한
    • 성능 목표보다 우선 순위 높음
    • 목표 달성이 어렵다면 힙 크기를 조금씩 줄여나가면서 목표를 이루려고 시도함
      • 대신 힙이 금방 가득 차면서 잦은 GC 호출로 인해 성능이 저하될 수 있음
  2. 성능 목표 (Throughput goal)
    • -XX:GCTimeRatio=
    • 값을 그대로 사용하지 않고 수식으로 계산된 비율만큼 전체 실행 시간에서 GC를 실행함
      • 수식: 1 / (1 + NNN)
      • 예: NNN=19 일 때는 1 / (1 + 19) = 0.05 (5%)
    • 기본값은 알고리즘에 따라 다름
      • Throughput Collector는 1%
      • CMS는 5%
      • G1은 8%
    • 이 목표를 달성하지 못하면 힙 크기를 조금씩 증가시키면서 GC가 일어나는 간격을 늘려서 목표를 달성하려고 시도함
    • 중지 시간이 늘어날 수 있음
  3. 튜닝 결과 확인
    • 성능 목표는 달성했지만 중지 시간이 길다면 최대 중지 시간 목표를 조정
      • 최대 중지 시간 목표와 성능 목표는 트레이드오프(trade-off)이므로 최적값을 찾아야 함
    • 기본 설정된 힙을 최대로 사용해도 목표를 달성하지 못했다면 힙 크기 조정
      • 필요하다면 힙 크기 외에 Generation별 크기를 조정할 필요가 있음
    • 목표를 달성하지 못했다면 사용 중인 서버에서 이룰 수 없는 목표는 아닌지 확인
      • 필요한 경우 좀 더 복잡한 튜닝 시도

2. GC에 대한 이해

  1. 힙 구조
  2. GC 기본 동작 과정
  3. GC 알고리즘별 특징과 차이점

2_1. 힙은 여러 Generation을 나눠져있음

2_2. 힙이 나눠진 이유는 ?

  1. Weak Generational Hypothesis
    • 대부분의 객체는 아주 짧은 시간동안 사용됨
    • 소수의 객체는 오랜 시간 사용됨
  2. GC 성능은 사용 중인 객체의 수에 크게 영향을 받음
    • Generation 구분이 없다면 빈번하게 만들어지고 사라지는 객체를 처리하기 위해 오랫동안 살아있는 객체들을 매번 확인해야함

2_3. Young Generation

  1. Eden
    • (대부분의) 객체가 생성되는 곳
  2. Survivor Space
    • Eden에서 살아남은 객체가 잠시동안 머무는 곳
    • 두 개의 Survivor Space가 존재함

2_4. Old Generation

  • 오랫동안 살아남은 객체가 옮겨지는 곳

2_5. Generation 별 GC 동작 방식

  1. Young Collection (Minor GC)
    • Eden이 가득찼을 때 발생
    • 항상 STW 유발
    • 사용하지 않는 객체가 많으므로 Old Collection과 비교해서 상대적으로 매우 빠름
  2. Old Collection (Major GC, Full GC)
    • Old Generation이 가득찼을 때 (또는 일정 이상 찼을 때) 발생
    • 알고리즘에 따라 항상 STW가 발생하거나 덜 발생함
    • Young Collection에 비해 느림

2_6. GC의 기본동작 3가지

  1. mark
    • 사용 중인 객체 표시
  2. sweep
    • 사용하지 않는 객체 제거
  3. compaction
    • 모든 객체들이 연속된 공간에 위치하도록 재배치함

3. GC 기본 동작

3_1. Young Collection

  1. Filling the Eden Space
    • Eden에서 새로운 객체가 만들어짐
  2. Object Aging
    • Eden이 가득차면 Young Collection이 일어나며 Eden을 비움
    • 사용하지 않는 객체는 지워짐
    • 사용 중인 객체는 바로 Old Generation으로 가지 않고 Survivor Space에서 머뭄
    • Survivor Space는 매 GC 사이클마다 From, To 역할을 번갈아가면서 수행
    • Survivor Space 한 곳으로 사용 중인 객체를 옮기면서 자연스럽게 Compaction도 하게됨
  3. Promotion
    • 충분히 나이를 먹은 객체는 Old Generation으로 보내짐 → Promotion
    • 만약 To Survivor Space가 가득차서 더 이상 객체를 받을 수 없다면 바로 Old Generation으로 보내짐 → Premature Promotion
    • Premature Promotion은 Old Collection에 영향을 주므로 최대한 피하는 게 좋음

3_2. Old Collection

  • 사용 중인 객체가 많으면 GC에 많은 시간이 걸림
  • 알고리즘에 따라서 다르게 동작하고 사용 목적, 튜닝 방법도 다름

3_3. 힙 크기와 Generation 크기 튜닝

  1. 대부분의 객체가 잠깐동안 사용되고 사라짐
    • Young Generation의 크기를 늘려주면 성능이 향상될 수 있음
  2. 대부분의 객체가 오랫동안 사용됨
    • Young Generation의 크기를 줄이거나
    • Promotion이 빨리 되도록 하면 성능이 향상될 수 있음
  • 최소 힙 크기
    • XmsN
  • 최대 힙 크기
    • XmxN
  • Generation 크기 조절 - -XX:NewRatio=3 - (Young : Old = 1 : 3, Young Generation은 전체 힙 크기에서 1 / 4)
    • -XX:SurvivorRatio=6
      • (Eden : Survivor = 1 : 6, 각 Survivor Space는 Young Generation의 1/8)
  • Promotion 비율 조절
    • -XX:MaxTenuringThreshold=
    • 기본값은 15
  1. GC 로그 옵션 플래그
    • 간단한 GC 로그 - -verbose:gc or -XX:+PrintGC
    • 더 자세한 로그 - -XX:+PrintGCDetails
    • 힙 크기 조정 로그 - -XX:-PrintAdaptiveSizePolicy
    • Tenuring Information - -XX:-PrintTenuringDistribution
    • 시간 정보 - -XX:+PrintGCTimeStamps -XX:+PrintGCDateStamps
    • Log rotation - -XX:+UseGCLogFileRotation -XX:NumberOfGCLogFiles=N -XX:GCLogFileSize=N
  2. 기타 유용한 툴
    • 튜닝 플래그 확인 - jcmd <process_id> VM.flags [-all] - java <other_option> -XX:+PrintFlagsFinal -version

    • 실행 커맨드 확인 - jcmd <process_id> VM.command_line

    • 실행 중 튜닝 플래그 바꾸기 (제한적으로 가능) - jinfo -flags +PrintGCDetails <process_id>


4.GC 알고리즘

  • Serial Collector
  • Throughput Collector
  • Concurrent Collector
    • CMS Collector
    • G1 Collector

4_1. Serial Collector

  • 가장 단순한 형태의 Collector로 단일 쓰레드로 동작
  • 시스템이 클라이언트 클래스라면 디폴트로 사용
  • Young Collection, Old Collection 모두 STW 발생
  • GC는 mark, sweep, compaction를 거쳐서 힙을 완전히 압축함
  • java -XX:+UseSerialGC -jar my-app.jar …

  • 사용할 때
    • Serial Collector: 힙 크기가 100MB 미만
    • 그 외 경우에는 Throughput Collector와 Concurrent Collector 중 하나를 고르게 됨

4_2. Throughtput Collector

  • Parallel Collector라고도 부름
  • 시스템이 server class라면 디폴트로 사용
  • 멀티 쓰레드로 동작
  • Young Collection, Old Collection 모두 STW 발생
  • GC는 mark, sweep, compact를 거쳐서 힙을 완전히 압축함
  • java -XX:+UseParallelGC -XX:+UseParallelOldGC -jar my-app.jar …
  • JDK 7u4 이상부터는 UseParallelGC 플래그를 사용하면 기본적으로 UseParallelOldGC 플래그를 사용함

4_3. Concurrenct Collector

  • 멀티 쓰레드로 동작
  • Young Collection은 STW 발생
  • Old Collection은 mark 과정 일부에서 STW 발생시키고 나머지는 프로그램 쓰레드와 동시에 진행됨
  • 따라서 중지 시간이 짧지만 (응답 속도가 빠름) 백그라운드 쓰레드를 위한 CPU 자원이 필요함
  • Old Collection은 Old Generation이 일정 이상 차기 시작했을 때 시작함
  • 대표적으로 CMS와 G1가 있음

4_3_1. CMS Collector

  • CMS는 “Concurrent Mark Sweep”의 줄임말
  • Old Generation을 압축하지 않아서 힙 단편화가 발생 (compaction 없음)
  • CMS에서 주의해야할 부분은 Concurrent Mode Failure로 두 가지 이유 때문에 발생
    1. 힙 단편화로 더 이상 객체를 할당할 수 없거나
    2. Old Collection이 끝나기 전에 많은 Promotion으로 Old Generation이
  • 가득차면 Full GC가 시작되면서 성능이 크게 저하됨 (이 때 힙을 압축함)
  • java -XX:+UseConcMarkSweepGC -XX:+UseParNewGC -jar my-app.jar …

4_3_2. G1 Collector

  • G1은 “Garbage First”의 줄임말
  • JDK 6, 7에서는 시험적으로 사용
  • JDK 8u40부터 production 환경에서 사용 가능할 정도로 안정화됨
  • JDK 9부터는 기본적으로 G1 Collector를 사용
    • -XX:MaxGCPauseMillis=200
  • G1 GC의 목표
    • 4GB 이상의 큰 힙을 빠르게 처리
    • 중지 시간을 설정하고 예측 가능
  • 장기적으로 CMS를 대체하려는 목적도 있음
  • Old Generation을 압축하지 않아서 힙 단편화가 발생 (compaction 없음)
  • CMS처럼 Concurrent Mode Failure가 발생할 수 있음
  • java -XX:+UseG1GC -jar my-app.jar …

  • 중요한 특징들
    • 힙을 Region (1MB ~ 32MB)로 나눔
    • 각 Region은 정해진 목적없이 필요할 때 Eden, Survivor Space, Old Generation이 될 수 있음
    • Generation별 Region은 인접할 필요없음
    • Young Collection의 기본 동작은 다른 알고리즘과 차이 없음
  • G1 GC의 Young Collection
    • G1 GC는 기본적으로 GC 작업 중 사용 중인 객체를 하나의 Region으로 모으려고 함 (부분적인 Compaction)
    • 그 외에 다른 알고리즘과의 큰 차이점은 없음
  • G1 GC의 Old Collection
    • 최대 중지 시간 목표 안에서 가장 Garbage가 많은 Region을 우선적으로 비움
    • 모든 Garbage를 다 처리하지 않을 수 있음
    • 사용 중인 객체를 하나의 Region으로 모으는 작업으로 부분적인 compaction 효과 있음

5. GC 알고리즘 비교

  • Throughput Collector가 좋을 때
    • 충분한 CPU가 없음
    • Concurrent Collector가 사용할 CPU 자원이 없음
    • 평균 응답 시간이 90 percentile, 99 percentile보다 더 중요할 때
    • 또는 응답 시간이 중요하지 않을 때
  • Concurrent Collector가 좋을 때
    • CPU를 충분히 사용할 수 있음
    • 평균 응답 시간보다 90 percentile, 99 percentile이 더 중요할 때
    • 힙이 커서 Throughput Collector가 Old Collection 작업에 오랜 시간이 걸릴 때
  • CMS vs G1
    • CMS가 나을 때 - 힙이 크지 않을 때 - JDK 8u40 보다 낮은 버전을 사용 중일 때
    • G1이 나을 때 - 힙이 4GB 이상으로 클 때 - 다음 조건을 하나 이상 만족하는 경우 CMS에서 G1으로 옮겼을 때 이득을볼 수 있음 - 50% 이상의 힙이 살아있는 객체로 채워짐 - 객체 할당 비율이나 promotion 비율이 크게 달라짐 - 애플리케이션의 GC로 인한 중지 시간이 0.5초에서 1초 이상