본문 바로가기

Study

TIL - G1GC vs ZGC : GC 방식 차이와 선택 기준

728x90
반응형

📌 한 줄 요약

G1GC는 처리량 최적화, ZGC는 latency 최적화. 힙이 커질수록 G1GC의 STW pause가 늘어나지만 ZGC는 힙 크기에 관계없이 항상 1-10ms 이하를 유지한다.


🔎 왜 GC가 중요한가 — STW(Stop-The-World)

JVM GC가 실행되는 순간 모든 애플리케이션 스레드가 멈춘다(STW). 100개 VU가 요청을 처리하는 도중 GC가 터지면, 100개 요청 모두가 그 시간만큼 추가 지연된다.

GC 발생 시 타임라인:
  VU 1   ──[요청 처리 중]── ⚡GC START ── 멈춤 450ms ── GC END ── 완료
  VU 2   ──[요청 처리 중]── ⚡GC START ── 멈춤 450ms ── GC END ── 완료
  ...
  VU 100 ──[요청 처리 중]── ⚡GC START ── 멈춤 450ms ── GC END ── 완료

→ 100개 요청 모두 +450ms 추가됨 → p95, max가 폭등

⚙️ G1GC (Garbage First GC)

작동 방식

힙을 동일한 크기의 Region 단위로 나누고, 가비지가 가장 많은 Region부터 수집한다.

힙 구조:
┌────┬────┬────┬────┬────┬────┐
│Eden│Eden│Surv│ Old│ Old│Eden│  ← 모두 같은 크기의 Region
└────┴────┴────┴────┴────┴────┘

GC 단계:
  Young GC:       Eden + Survivor 수집  → STW, 10-200ms
  Concurrent Mark: Old Gen 가비지 표시  → 백그라운드, 앱 실행 중
  Mixed GC:        Young + Old Gen 수집 → STW, 50-800ms
  Full GC:         전체 힙              → STW, 1-5초 (최후 수단)

핵심 설정: IHOP (InitiatingHeapOccupancyPercent)

G1GC가 Old Gen 청소(Concurrent Mark)를 언제 시작할지 결정하는 임계치. G1GC 전용 설정이며 ZGC에는 없다.

기본값 IHOP = 45%

힙 577MB 기준:
  Old Gen이 577MB × 45% = 260MB 넘어야 Concurrent Mark 시작

실제 문제:
  Old Gen 122MB = 21% → G1: "아직 여유 있어" → Concurrent Mark 안 함
  → 잔류 객체 누적 → Mixed GC 발생 시 STW 수백ms 폭탄

개선: -XX:InitiatingHeapOccupancyPercent=20 (더 일찍 청소 시작)


⚡️ ZGC (Z Garbage Collector)

작동 방식

ZGC는 GC 작업 대부분을 애플리케이션과 동시에(Concurrently) 수행한다. IHOP 같은 임계치 개념이 없고, 힙 사용률을 지속적으로 모니터링하면서 알아서 타이밍을 잡아 백그라운드에서 청소한다.

G1GC:  앱 실행 ─── ⚡STW 200ms 멈춤 ─── 앱 재개
ZGC:   앱 실행 ────────────────────────── 앱 실행
                 (GC가 동시에 백그라운드에서 청소)
       STW는 딱 1-3ms (thread stack scan 등 최소한만)

 

 


📊 G1GC vs ZGC 비교

🔧 설정 방법

G1GC (현재)

JAVA_TOOL_OPTIONS: "-XX:+UseG1GC
  -XX:MaxGCPauseMillis=20
  -XX:MaxRAMPercentage=70.0
  -XX:InitiatingHeapOccupancyPercent=20
  -XX:-ExplicitGCInvokesConcurrent"

ZGC 전환 시

JAVA_TOOL_OPTIONS: "-XX:+UseZGC
  -XX:MaxRAMPercentage=70.0
  -Xlog:gc*:file=/tmp/gc.log:time,uptime,level,tags:filecount=5,filesize=20m"

MaxGCPauseMillis, InitiatingHeapOccupancyPercent, ExplicitGCInvokesConcurrent 전부 필요 없어짐.


🧪 실제 관찰한 G1GC 문제 (부하 테스트 중)

부하 테스트를 반복하면서 G1GC 문제를 직접 겪었다.

테스트 3회차 (컨테이너 재시작 없이):
  03:19:19 → GC Young  235ms ← 테스트 시작 전 GC 폭탄
  03:19:25 → GC Young  453ms
  03:19:26 → GC Young  286ms

결과: 69.8 req/s (정상 138.9의 절반), avg 1361ms, max 6984ms

원인: Old Gen 122MB = 21%, IHOP 45% 미달 → Concurrent Mark 미실행 → Young GC가 Old Gen remembered set을 많이 스캔하느라 pause 폭증.

ZGC였다면: pause 1-5ms → max response time 수백ms 이하 유지.


🚫 ZGC가 손해인 상황 vs ✅ 유리한 상황

ZGC를 쓰면 손해인 상황

현재 쿠폰 서비스가 ZGC에 유리한 이유

G1GC 문제 실측:
  avg 651ms, p90 1287ms, p95 1469ms, max 2101ms
  → avg와 max 차이 = 1450ms (GC pause 영향)

ZGC 기대치:
  max STW = 1-10ms → max response time ≈ avg + 수십ms 이내

💡 교훈

  1. G1GC의 MaxGCPauseMillis=20은 목표값이지 보장값이 아니다. Mixed GC 시 수백ms를 넘길 수 있다.
  2. IHOP은 G1GC 전용 설정이다. ZGC에는 없고, ZGC는 스스로 GC 타이밍을 결정한다.
  3. ZGC는 설정이 거의 없다는 게 장점이다. IHOP, region size, pause 목표 등의 복잡한 튜닝이 필요 없다.
  4. 부하 테스트에서 max가 비정상적으로 크면 GC를 의심하라. avg는 정상인데 p95/max가 수초라면, 그 순간 GC가 터진 것이다.
  5. API 서버는 ZGC가 더 적합하다. 처리량보다 응답 시간 일관성이 더 중요하기 때문이다.
반응형