GC가 뭔가 돌아가고 있다는 건 알겠는데, 로그를 열면 암호 같은 텍스트만 가득합니다. 이 글에서는 GC 로그를 어떻게 설정하고, 어떻게 읽고, 어떤 도구로 시각화해서, 무엇을 튜닝할지 결정하는지 전 과정을 단계별로 정리합니다. 앞서 다룬 ZGC vs G1GC, Heap Dump 분석에 이어 "GC가 실제로 어떻게 동작하고 있는지" 직접 눈으로 확인하는 방법을 익히는 글입니다.
👉 ZGC vs G1GC — Off-Heap 메모리 해제 차이 완전 정리
Table of Contents
1. GC 로그가 왜 필요한가요?
GC 튜닝을 시작하는 많은 분들이 이런 경험을 합니다.
# 서버 응답 시간 모니터링 중
[13:42:01] p99 latency: 45ms <- 평소 수준
[13:42:08] p99 latency: 2300ms <- 갑자기 2초 이상 급등
[13:42:09] p99 latency: 48ms <- 곧 복구
# 원인 파악 시도
- CPU 사용률: 정상
- 네트워크 I/O: 정상
- DB 슬로우 쿼리: 없음
- ???Code language: PHP (php)
이런 순간적인 지연 급등의 원인 중 상당수가 GC Stop-The-World(STW) 입니다. STW는 GC가 동작하는 동안 모든 애플리케이션 스레드가 멈추는 현상인데, GC 로그 없이는 언제, 얼마나 멈췄는지 전혀 알 수 없습니다.
핵심 용어 정리
용어 설명 STW (Stop-The-World) GC 실행 중 모든 앱 스레드가 멈추는 시간 Minor GC Young Generation(Eden + Survivor)만 수거하는 빠른 GC Major GC Old Generation을 수거하는 느린 GC Full GC Heap 전체를 수거. 가장 오래 걸리며 STW도 가장 김 GC Pause STW로 인해 애플리케이션이 실제로 멈춘 시간 Throughput 전체 시간 중 GC가 아닌 앱 실행 시간의 비율 (높을수록 좋음) Footprint GC가 사용하는 메모리 총량
2. GC 로그 출력 설정 — Java 9 이후 통합 로깅
Java 9부터 GC 로그 옵션이 -Xlog:gc* 체계로 통합되었습니다. 이전 방식(-XX:+PrintGCDetails 등)은 deprecated이므로 새 방식을 사용합니다.
기본 설정 (개발/스테이징 환경)
java \
-Xms2g \
-Xmx2g \
-XX:+UseG1GC \
# GC 기본 정보 + 상세 + Heap 통계 출력
-Xlog:gc*:file=/var/log/app/gc.log:time,uptime,level,tags \
# 로그 파일 롤링 설정 (파일당 20MB, 최대 10개 보관)
-Xlog:gc*:file=/var/log/app/gc.log:time,uptime,level,tags:filecount=10,filesize=20m \
-jar app.jarCode language: PHP (php)
운영 환경 권장 설정
운영 환경에서는 로그 양을 줄이되, 분석에 필요한 핵심 정보는 유지합니다.
java \
-Xms4g -Xmx4g \
-XX:+UseG1GC \
-XX:MaxGCPauseMillis=200 \
# gc+heap: Heap 변화량 포함, gc+age: Survivor 세대 정보 포함
-Xlog:gc+heap=info,gc+age=debug,gc*=info:file=/var/log/app/gc-%t.log:time,uptime,pid,level,tags:filecount=5,filesize=50m \
-jar app.jarCode language: PHP (php)
%t를 파일명에 넣으면 JVM 시작 시각이 파일명에 포함되어 재시작 전후 로그를 구분하기 편합니다.
application.yml에서 관리하기 (Spring Boot)
실제 운영에서는 JVM 옵션을 환경 변수나 실행 스크립트에서 관리하는 게 일반적이지만, Docker 환경이라면 JAVA_TOOL_OPTIONS로 일원화할 수 있습니다.
# docker-compose.yml
services:
app:
image: my-spring-app:latest
environment:
JAVA_TOOL_OPTIONS: >-
-Xms2g -Xmx2g
-XX:+UseG1GC
-XX:MaxGCPauseMillis=200
-Xlog:gc*:file=/var/log/gc/gc.log:time,uptime,level,tags:filecount=5,filesize=20m
volumes:
- ./logs/gc:/var/log/gc # GC 로그 호스트에 마운트Code language: PHP (php)
3. GC 로그 직접 읽기 — 구조 이해
도구를 쓰기 전에, 로그가 어떤 구조인지 한 번은 직접 읽어보는 게 중요합니다.
G1GC 로그 읽기
# 형식: [시각][JVM 구동 시간][레벨][태그] 내용
[2025-04-01T13:42:07.123+0900][10.456s][info][gc] GC(42) Pause Young (Normal) (G1 Evacuation Pause) 512M->498M(2048M) 12.345ms
^ ^ ^ ^
GC 횟수 GC 종류 Heap 변화 전체 Heap STW 시간
[2025-04-01T13:42:07.124+0900][10.457s][info][gc,heap] GC(42) Eden regions: 256->0(256)
# Eden: 256개 리전 사용 → GC 후 0개 (신규 할당 가능 공간: 256개)
[2025-04-01T13:42:07.124+0900][10.457s][info][gc,heap] GC(42) Survivor regions: 5->8(32)
# Survivor: 5개 → 8개 (Young GC에서 살아남은 객체 증가)
[2025-04-01T13:42:07.124+0900][10.457s][info][gc,heap] GC(42) Old regions: 120->118(512)
# Old Gen: 120개 → 118개 (일부 수거됨)
# Full GC 발생 — STW가 길다
[2025-04-01T13:43:15.001+0900][78.334s][info][gc] GC(89) Pause Full (G1 Humongous Allocation) 1900M->1200M(2048M) 2134.567ms
# ^ ^
# Full GC 원인: 대형 객체 할당 STW 2.1초!Code language: CSS (css)
ZGC 로그 읽기
ZGC는 대부분의 작업을 동시(Concurrent)로 처리하므로 로그 패턴이 다릅니다.
# ZGC: STW는 매우 짧고, 대부분 Concurrent 단계
[2025-04-01T13:42:07.100+0900][info][gc] GC(15) Garbage Collection (Proactive)
[2025-04-01T13:42:07.101+0900][info][gc,phases] GC(15) Pause Mark Start 0.023ms <- STW (매우 짧음)
[2025-04-01T13:42:07.250+0900][info][gc,phases] GC(15) Concurrent Mark 149.2ms <- STW 없음
[2025-04-01T13:42:07.251+0900][info][gc,phases] GC(15) Pause Mark End 0.015ms <- STW (매우 짧음)
[2025-04-01T13:42:07.380+0900][info][gc,phases] GC(15) Concurrent Relocate 129.1ms <- STW 없음
[2025-04-01T13:42:07.381+0900][info][gc] GC(15) Load: 2.5/2.3/2.1
[2025-04-01T13:42:07.381+0900][info][gc] GC(15) MMU: 2ms/98.5%, 5ms/99.2%, 10ms/99.6%
# ^ 2ms 윈도우 내 앱 실행 비율 98.5%Code language: PHP (php)
ZGC에서 MMU(Minimum Mutator Utilization) 가 핵심 지표입니다.
10ms/99.6%는 "10ms 슬라이딩 윈도우에서 앱이 99.6% 시간 동안 실행됐다"는 뜻입니다. 이 수치가 낮아진다면 ZGC임에도 레이턴시 영향이 생기고 있다는 신호입니다.
4. GCViewer로 시각화하기 — 로컬 분석
GCViewer는 GC 로그 파일을 열어 시각화해주는 무료 데스크톱 도구입니다. 인터넷 없이 로컬에서 분석할 수 있어 운영 Heap Dump처럼 민감한 환경에 적합합니다.
설치 및 실행
# GitHub에서 최신 JAR 다운로드
# https://github.com/chewiebug/GCViewer/releases
wget https://github.com/chewiebug/GCViewer/releases/download/1.36/gcviewer-1.36.jar
# 실행 (Java 11 이상 필요)
java -jar gcviewer-1.36.jar
# 또는 로그 파일을 인자로 직접 열기
java -jar gcviewer-1.36.jar /var/log/app/gc.logCode language: PHP (php)
GCViewer에서 봐야 할 핵심 지표
정상 패턴 vs 비정상 패턴
정상 Heap 패턴 (톱니 모양):
Heap ▲
│ /\ /\ /\ /\
│ / \/ \/ \/ \
└──────────────────▶ 시간
GC가 주기적으로 잘 수거하고 있음
누수 의심 패턴 (우상향):
Heap ▲ /\
│ /\/ \
│ /\/
│/
└──────────────────▶ 시간
GC를 해도 기저 메모리가 계속 증가 → 메모리 누수 의심
Full GC 과다 패턴:
Heap ▲
│████████████████ <- Old Gen 거의 가득
│ | | | | <- Full GC 반복 (수직 낙하)
└──────────────────▶ 시간
Heap이 너무 작거나, 장기 생존 객체가 너무 많음
5. GCEasy로 분석하기 — 웹 기반 자동 리포트
GCEasy는 GC 로그 파일을 업로드하면 자동으로 분석 리포트를 생성해주는 웹 서비스입니다. 무료 플랜으로도 핵심 분석이 가능합니다.
운영 GC 로그에 민감한 정보는 없지만, 사내 보안 정책에 따라 외부 업로드 여부를 확인하세요. 민감한 환경이라면 GCViewer(로컬) 또는 아래 GCEasy API 방식을 활용하세요.
웹 UI 사용법
1. https://gceasy.io 접속
2. GC 로그 파일 업로드 (.log, .txt 모두 가능)
3. "Analyze" 클릭
4. 자동 생성된 리포트 확인Code language: JavaScript (javascript)
API로 자동화 (CI/CD 통합)
GCEasy는 REST API도 제공합니다. 부하 테스트 후 자동으로 GC 리포트를 생성하는 파이프라인을 만들 수 있습니다.
#!/bin/bash
# gc-report.sh — 부하 테스트 후 GCEasy API로 리포트 자동 생성
GC_LOG_PATH="/var/log/app/gc.log"
GCEASY_API_KEY="your-api-key" # https://gceasy.io 무료 가입 후 발급
REPORT_OUTPUT="/var/log/gc-report.json"
echo "GC 로그 업로드 중..."
curl -s \
-X POST \
--data-binary @"$GC_LOG_PATH" \
"https://api.gceasy.io/analyzeGC?apiKey=${GCEASY_API_KEY}" \
-H "Content-Type: text/plain" \
-o "$REPORT_OUTPUT"
# 핵심 수치 추출
THROUGHPUT=$(jq '.throughput' "$REPORT_OUTPUT")
MAX_PAUSE=$(jq '.gcKPI.maxGCPauseTime' "$REPORT_OUTPUT")
FULL_GC_COUNT=$(jq '.gcKPI.fullGCCount' "$REPORT_OUTPUT")
echo "===== GC 분석 결과 ====="
echo "Throughput : ${THROUGHPUT}%"
echo "Max Pause : ${MAX_PAUSE}ms"
echo "Full GC 횟수 : ${FULL_GC_COUNT}회"
# Full GC가 10회 이상이면 경고
if [ "$FULL_GC_COUNT" -gt 10 ]; then
echo "Full GC 과다 발생! Old Gen 설정 또는 메모리 누수 점검 필요"
exit 1
fi
echo "GC 상태 양호"Code language: PHP (php)
6. GCViewer vs GCEasy 비교
| 항목 | GCViewer | GCEasy |
|---|---|---|
| 방식 | 로컬 데스크톱 앱 | 웹 서비스 |
| 비용 | 무료 (오픈소스) | 무료 플랜 있음 (제한적) |
| 보안 | 로그 외부 미전송 | 외부 서버 업로드 |
| 분석 깊이 | 기본 시각화 중심 | AI 기반 권고사항 포함 |
| API 지원 | 없음 | REST API 지원 |
| 지원 GC | G1GC, CMS, Serial 등 | G1GC, ZGC, Shenandoah 등 |
| 추천 상황 | 빠른 로컬 분석, 보안 민감 환경 | 상세 자동 리포트, CI 통합 |
7. 분석 결과로 실제 튜닝하기
케이스 1: Full GC 빈발 — Old Gen이 너무 작다
GCViewer 결과:
- Throughput: 78% <- 기준 95% 미달
- Full GC 횟수: 47회 (6시간 기준)
- Max Pause: 4,230ms
원인 분석 및 해결:
# 현재 설정 (문제 상황)
java -Xms2g -Xmx2g -XX:+UseG1GC -jar app.jar
# 개선 1: Heap 크기 증가
java -Xms4g -Xmx4g -XX:+UseG1GC -jar app.jar
# 개선 2: G1GC 점유율 조정 (Old Gen 비율 늘리기)
java -Xms4g -Xmx4g \
-XX:+UseG1GC \
-XX:NewRatio=2 \ # Old:Young = 2:1 비율 (기본값)
-XX:G1HeapRegionSize=16m \ # 리전 크기 명시 (큰 객체 처리 개선)
-XX:InitiatingHeapOccupancyPercent=35 \ # Old Gen 35%에서 Concurrent GC 시작 (기본 45%)
-jar app.jarCode language: PHP (php)
InitiatingHeapOccupancyPercent를 낮추면 더 일찍 GC를 시작해 Full GC 전에 Old Gen을 비울 수 있습니다. 단, GC 빈도가 증가할 수 있어 적절한 값을 찾아야 합니다.
케이스 2: STW가 목표보다 길다 — G1GC Pause 목표 조정
GCViewer 결과:
- Avg Pause: 350ms <- SLA 기준 200ms 초과
- Max Pause: 980ms
- Throughput: 92% <- 그나마 양호
해결:
java -Xms4g -Xmx4g \
-XX:+UseG1GC \
# G1GC에게 "최대한 200ms 이내로 STW를 끝내줘"라고 목표 설정
# JVM이 이 목표에 맞게 GC 리전 개수를 자동 조절
-XX:MaxGCPauseMillis=200 \
-jar app.jarCode language: PHP (php)
MaxGCPauseMillis는 목표값이지 보장값이 아닙니다. 값을 너무 낮게 설정하면 GC가 충분히 수거하지 못해 오히려 Full GC가 더 자주 발생할 수 있습니다.
케이스 3: ZGC인데 레이턴시가 튄다 — Allocation Stall 의심
ZGC 로그에서 아래 메시지가 보인다면:
[warn][gc] Allocation Stall (Thread "http-nio-8080-exec-3") 234.567msCode language: CSS (css)
Allocation Stall은 GC가 메모리를 확보하는 속도보다 애플리케이션이 할당하는 속도가 더 빠를 때 발생합니다. ZGC의 주요 레이턴시 원인 중 하나입니다.
# 해결 방법 1: Heap 크기 증가 (ZGC는 여유 공간이 많을수록 좋음)
java -Xms8g -Xmx8g -XX:+UseZGC -jar app.jar
# 해결 방법 2: GC를 더 자주 실행 (Proactive GC 트리거 조정)
java -Xms8g -Xmx8g \
-XX:+UseZGC \
-XX:ZCollectionInterval=5 \ # 5초마다 한 번씩 GC 실행 (기본값: 0 = 필요할 때만)
-jar app.jarCode language: PHP (php)
8. 전체 GC 튜닝 워크플로우
9. 결과 및 검증
튜닝 전후를 같은 부하 조건에서 비교하는 것이 중요합니다.
# 튜닝 전후 핵심 지표 비교 체크리스트
# 1. GCViewer Summary 탭에서 수치 비교
항목 | 튜닝 전 | 튜닝 후 | 기준
Throughput | 78% | 96% | >= 95%
Avg Pause | 350ms | 85ms | <= 200ms
Max Pause | 980ms | 210ms | <= 500ms
Full GC 횟수 | 47회 | 1회 | <= 3회
# 2. Prometheus로 시계열 비교 (Spring Boot Actuator 연동 시)
# GC Pause Time 총합 (초당)
rate(jvm_gc_pause_seconds_sum[5m])
# GC 발생 횟수 (분당)
rate(jvm_gc_pause_seconds_count[1m])
# Old Gen 사용률
jvm_memory_used_bytes{area="heap", id=~".*old.*"}
/ jvm_memory_max_bytes{area="heap", id=~".*old.*"}Code language: PHP (php)
10. 추가 고려사항
추가로 알아두면 좋은 내용들입니다.
GC 로그는 항상 켜두는 게 좋습니다
-Xlog:gc*의 오버헤드는 1% 미만입니다. "문제가 생기면 켜야지"라고 생각하면 정작 문제 발생 시 로그가 없어 분석이 불가능합니다. 운영 환경에서도 항상 활성화를 권장합니다.
Humongous Allocation 주의 (G1GC)
G1GC에서 리전 크기의 50%를 초과하는 객체(Humongous Object)는 별도 처리됩니다. 이로 인해 Full GC가 발생하는 경우가 많습니다. 로그에서 G1 Humongous Allocation 메시지가 자주 보인다면 -XX:G1HeapRegionSize를 늘려서 해결할 수 있습니다.
# Humongous Allocation 빈도 확인
grep "Humongous" /var/log/app/gc.log | wc -l
# 리전 크기 늘리기 (1, 2, 4, 8, 16, 32MB 중 선택)
-XX:G1HeapRegionSize=32mCode language: PHP (php)
JVM 버전에 따른 GC 기본값 변화
| Java 버전 | 기본 GC |
|---|---|
| Java 8 | Parallel GC |
| Java 9~14 | G1GC |
| Java 15+ | G1GC (ZGC/Shenandoah는 명시 필요) |
| Java 21 | G1GC (ZGC 프로덕션 준비 완료) |
👉 ZGC vs G1GC — Off-Heap 메모리 해제 차이 완전 정리
👉 JDK Flight Recorder + JDK Mission Control 실전 — 운영 중 JVM 프로파일링
11. 마무리
이번 글을 세 줄로 정리합니다.
- GC 로그는
-Xlog:gc*로 항상 활성화해 두어야 레이턴시 급등의 원인을 사후에 추적할 수 있습니다. - GCViewer(로컬 보안 환경)와 GCEasy(빠른 자동 리포트)를 상황에 따라 조합해 Throughput, Max Pause, Full GC 횟수를 핵심 지표로 분석합니다.
- 튜닝은 반드시 동일 부하 조건에서 전후 비교로 검증하고, Heap 우상향 패턴이 보이면 GC 튜닝보다 Heap Dump 분석을 먼저 진행하세요.
다음 글에서는 JDK Flight Recorder(JFR) + JDK Mission Control(JMC) 로 GC 로그보다 더 깊이, 운영 서버에서 실시간으로 메모리 할당 핫스팟과 스레드 병목을 찾는 방법을 다루겠습니다.
조회수: 1