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 이내
💡 교훈
- G1GC의 MaxGCPauseMillis=20은 목표값이지 보장값이 아니다. Mixed GC 시 수백ms를 넘길 수 있다.
- IHOP은 G1GC 전용 설정이다. ZGC에는 없고, ZGC는 스스로 GC 타이밍을 결정한다.
- ZGC는 설정이 거의 없다는 게 장점이다. IHOP, region size, pause 목표 등의 복잡한 튜닝이 필요 없다.
- 부하 테스트에서 max가 비정상적으로 크면 GC를 의심하라. avg는 정상인데 p95/max가 수초라면, 그 순간 GC가 터진 것이다.
- API 서버는 ZGC가 더 적합하다. 처리량보다 응답 시간 일관성이 더 중요하기 때문이다.
반응형
'Study' 카테고리의 다른 글
| TIL - Docker restart 정책 비교 (no / on-failure / always / unless-stopped) (0) | 2026.07.22 |
|---|---|
| TIL - Boolean.TRUE.equals()를 쓰는 이유 (0) | 2026.07.21 |
| [내일배움캠프 TIL] - Lettuce Redis 명령이 Zipkin에 안 나타나는 이유 & 해결 (0) | 2026.07.06 |
| [내일배움캠프 TIL] - 부하 테스트 지표와 p95가 국룰인 이유 (0) | 2026.07.03 |
| [내일배움캠프 TIL] - Redis 동시성 제어 3가지 방식 비교 (0) | 2026.07.02 |