GC 튜닝 실전 — G1과 ZGC 설정, GC 로그 분석까지
GC가 뭔지는 알겠는데, 운영 환경에서 GC 때문에 응답이 느려지면 어디서부터 손대야 할까?
GC의 종류와 동작 원리를 아는 것은 시작일 뿐이다. 실제로 성능 문제를 겪었을 때 GC 로그를 읽고, 원인을 파악하고, 적절한 파라미터를 조정할 수 있어야 한다. G1 GC와 ZGC를 중심으로 실전 튜닝을 다룬다.
GC 로그 활성화
Java 9+ (Unified Logging)
java -Xlog:gc*:file=gc.log:time,uptime,level,tags:filecount=5,filesize=10M \
-jar myapp.jar
| 옵션 | 의미 |
|---|---|
gc* | GC 관련 모든 로그 |
file=gc.log | 파일로 출력 |
time,uptime,level,tags | 타임스탬프, 가동 시간, 레벨, 태그 포함 |
filecount=5,filesize=10M | 최대 5개 파일, 각 10MB 로테이션 |
추가 유용한 옵션
# GC 일시 정지 시간 상세
-Xlog:gc+phases=debug
# 힙 영역 상세 (G1)
-Xlog:gc+heap=info
# GC 원인 표시
-Xlog:gc+cause=info
# 세이프포인트 정보
-Xlog:safepoint=info
GC 로그 읽는 법
G1 GC 로그 예시
[2026-03-19T10:30:15.123+0900] GC(42) Pause Young (Normal) (G1 Evacuation Pause)
[2026-03-19T10:30:15.123+0900] GC(42) Pre Evacuate Collection Set: 0.1ms
[2026-03-19T10:30:15.123+0900] GC(42) Merge Heap Roots: 0.2ms
[2026-03-19T10:30:15.123+0900] GC(42) Evacuate Collection Set: 8.5ms
[2026-03-19T10:30:15.123+0900] GC(42) Post Evacuate Collection Set: 1.2ms
[2026-03-19T10:30:15.123+0900] GC(42) Other: 0.3ms
[2026-03-19T10:30:15.123+0900] GC(42) Pause Young (Normal) 256M->48M(512M) 10.3ms
마지막 줄의 의미:
256M->48M: 사용 메모리가 256MB에서 48MB로 감소(512M): 전체 힙 크기10.3ms: GC 일시 정지 시간
주의해야 할 패턴
# Full GC — 이게 자주 보이면 문제
GC(99) Pause Full (Allocation Failure) 480M->200M(512M) 1523.4ms
# to-space exhausted — 대피 실패
GC(55) To-space exhausted
# Humongous allocation — 대형 객체 할당
GC(33) Pause Young (Concurrent Start) (G1 Humongous Allocation)
G1 GC 튜닝
G1은 Java 9부터 기본 GC이며, 대부분의 워크로드에 적합합니다.
핵심 파라미터
java \
-XX:+UseG1GC \ # G1 GC 사용 (Java 9+ 기본)
-Xms4g -Xmx4g \ # 힙 크기 (최소=최대 권장)
-XX:MaxGCPauseMillis=200 \ # 목표 일시 정지 시간 (기본 200ms)
-XX:G1HeapRegionSize=8m \ # 리전 크기 (1~32MB, 2의 거듭제곱)
-XX:InitiatingHeapOccupancyPercent=45 \ # IHOP: Concurrent Marking 시작 임계값
-XX:G1ReservePercent=10 \ # 대피 실패 방지용 예약 공간
-jar myapp.jar
시나리오별 튜닝
일시 정지 시간이 너무 길다
# 목표 일시 정지 시간 줄이기
-XX:MaxGCPauseMillis=100
# 너무 작게 설정하면 GC가 자주 발생하여 처리량이 떨어질 수 있음
# 50ms 이하는 신중하게 설정
Full GC가 발생한다
# IHOP를 낮춰서 Concurrent Marking을 더 일찍 시작
-XX:InitiatingHeapOccupancyPercent=35
# 힙 크기 증가 검토
-Xms8g -Xmx8g
# Concurrent Marking 스레드 증가
-XX:ConcGCThreads=4
Humongous 객체 문제
G1에서 리전 크기의 50% 이상인 객체는 Humongous 객체로 분류됩니다.
# 리전 크기를 늘려서 Humongous 할당 줄이기
-XX:G1HeapRegionSize=16m
# 또는 애플리케이션에서 대형 배열/객체 생성을 줄이기
to-space exhausted 발생
# 예약 공간 비율 증가
-XX:G1ReservePercent=15
# 힙 크기 증가
-Xmx8g
G1 GC 사이클 이해
Young GC (Eden 가득참)
↓ (반복)
Concurrent Marking Cycle (IHOP 도달 시)
1. Initial Mark (STW, Young GC와 동시)
2. Root Region Scan
3. Concurrent Mark (병행)
4. Remark (STW)
5. Cleanup (STW + 병행)
↓
Mixed GC (Young + 가비지 비율 높은 Old 리전)
↓ (필요 시 반복)
다시 Young GC로
ZGC 튜닝
ZGC는 저지연(low-latency) GC로, 힙 크기에 관계없이 밀리초 이하의 일시 정지를 목표로 합니다.
기본 설정
java \
-XX:+UseZGC \
-XX:+ZGenerational \ # Java 21+: 세대별 ZGC (권장)
-Xms4g -Xmx4g \
-XX:SoftMaxHeapSize=3g \ # 힙 사용량 소프트 제한
-jar myapp.jar
ZGC의 특성
| 특성 | 값 |
|---|---|
| 일시 정지 시간 | 일반적으로 1ms 미만 |
| 힙 크기 지원 | 수 MB ~ 16TB |
| 메모리 오버헤드 | 약 3~5% 추가 |
| 처리량 | G1 대비 약간 낮을 수 있음 |
ZGC 파라미터
# ZGC 동시성 스레드 수
-XX:ConcGCThreads=4
# 소프트 최대 힙 크기 — 이 크기 이하를 유지하도록 GC가 적극적으로 동작
-XX:SoftMaxHeapSize=3g
# 대형 페이지 사용 (성능 향상)
-XX:+UseLargePages
ZGC vs G1 선택 기준
| 기준 | G1 | ZGC |
|---|---|---|
| 일시 정지 허용 범위 | 수십~수백 ms | 수 ms 이하 필요 |
| 힙 크기 | 수 GB | 수십 GB 이상 |
| 처리량 우선 | 적합 | G1이 약간 유리할 수 있음 |
| 지연 시간 우선 | 보통 | 최적 |
GC 분석 도구
GCEasy (온라인)
GC 로그 파일을 업로드하면 자동으로 분석 리포트를 생성합니다.
- 일시 정지 시간 분포
- GC 원인 분석
- 힙 사용량 추세
- 튜닝 권장사항
GCViewer
# GC 로그를 시각화
java -jar gcviewer.jar gc.log
그래프로 힙 사용량 변화, GC 빈도, 일시 정지 시간 추이를 확인할 수 있습니다.
JDK Mission Control (JMC)
# JFR(Java Flight Recorder) 활성화
java -XX:StartFlightRecording=duration=60s,filename=recording.jfr \
-jar myapp.jar
JFR은 GC뿐 아니라 스레드, I/O, 메서드 프로파일링까지 통합적으로 분석할 수 있습니다.
실전 튜닝 체크리스트
1단계: 측정
# GC 로그 활성화 (운영 환경에서도 항상 켜두기)
-Xlog:gc*:file=gc.log:time,uptime,level,tags:filecount=10,filesize=50M
2단계: 분석
확인할 항목:
- **GC 빈도 **: Young GC가 초당 몇 번 발생하는가?
- ** 일시 정지 시간 **: P99 기준으로 허용 범위 내인가?
- **Full GC 유무 **: Full GC가 발생하면 원인 파악 필수
- ** 힙 사용량 추세 **: GC 후에도 사용량이 계속 증가하면 메모리 누수 의심
- Promotion rate: Young에서 Old로 승격되는 비율
3단계: 조정
# 일반적인 시작점
-Xms4g -Xmx4g # 힙 크기 고정
-XX:MaxGCPauseMillis=200 # 일시 정지 목표
-XX:+UseG1GC # 기본 GC
# 문제에 따라 하나씩 조정
# 변경 → 측정 → 분석 → 반복
4단계: 검증
- 부하 테스트 환경에서 변경 효과 확인
- 운영 배포 후 모니터링
- GC 로그 지속 수집
주의할 점
Xms와 Xmx를 다르게 설정하면 안 된다
힙이 늘어나는 과정에서 GC가 자주 발생한다. 리사이징 자체도 비용이므로, 운영 환경에서는 같은 값으로 설정해야 한다.
파라미터를 한꺼번에 여러 개 바꾸면 원인 파악이 불가능하다
어떤 변경이 효과가 있었는지 알 수 없다. 반드시 하나씩 변경하고 측정해야 한다.
GC 로그를 끄면 문제가 터졌을 때 대응할 수 없다
"성능에 영향을 줄까 봐" GC 로그를 비활성화하는 경우가 있지만, GC 로그의 오버헤드는 무시할 수 있을 정도로 작다. 운영 환경에서도 항상 켜두는 것이 원칙이다.
힙만 키우고 원인을 찾지 않으면 시한폭탄이다
메모리 누수가 원인이면 힙을 아무리 키워도 결국 OOM이 발생한다. 힙 증가는 임시 조치일 뿐, 반드시 GC 로그와 힙 덤프로 근본 원인을 파악해야 한다.
정리
| 항목 | 핵심 |
|---|---|
| 튜닝 첫 단계 | GC 로그 활성화 → 현재 상태 분석 |
| G1 핵심 파라미터 | MaxGCPauseMillis(목표 pause), InitiatingHeapOccupancyPercent(IHOP) |
| ZGC 특성 | 저지연(1ms 미만 pause), 대용량 힙에 적합, 설정이 단순 |
| G1 vs ZGC 선택 | 수십~수백 ms pause 허용 → G1, 수 ms 이하 필요 → ZGC |
| 튜닝 원칙 | 파라미터 하나씩 변경 → 측정 → 분석 사이클 반복 |
| GC 로그 | 운영 환경에서도 항상 활성화 (오버헤드 무시 가능) |
| 분석 도구 | GCEasy(온라인), GCViewer(그래프), JFR+JMC(통합 프로파일링) |