JVM GC 로그 분석 실전 — GCViewer와 GCEasy로 튜닝 시작하기

"☕" 9 "min read"

GC가 뭔가 돌아가고 있다는 건 알겠는데, 로그를 열면 암호 같은 텍스트만 가득합니다. 이 글에서는 GC 로그를 어떻게 설정하고, 어떻게 읽고, 어떤 도구로 시각화해서, 무엇을 튜닝할지 결정하는지 전 과정을 단계별로 정리합니다. 앞서 다룬 ZGC vs G1GC, Heap Dump 분석에 이어 "GC가 실제로 어떻게 동작하고 있는지" 직접 눈으로 확인하는 방법을 익히는 글입니다.

👉 ZGC vs G1GC — Off-Heap 메모리 해제 차이 완전 정리


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 GCYoung Generation(Eden + Survivor)만 수거하는 빠른 GC
Major GCOld Generation을 수거하는 느린 GC
Full GCHeap 전체를 수거. 가장 오래 걸리며 STW도 가장 김
GC PauseSTW로 인해 애플리케이션이 실제로 멈춘 시간
Throughput전체 시간 중 GC가 아닌 앱 실행 시간의 비율 (높을수록 좋음)
FootprintGC가 사용하는 메모리 총량
GC 로그 분석 흐름도레이턴시 급등 → GC 로그 STW 발견 → 튜닝 완료 3단계 흐름레이턴시 급등p99: 2,300ms원인 불명GC 로그 활성화-Xlog:gc*원인 발견Full GC STW2,134ms 검출GC 튜닝 적용MaxGCPause=200개선 완료p99: 50ms 이하Throughput 96%

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에서 봐야 할 핵심 지표

GCViewer 분석 플로우GC 로그 파일을 GCViewer에서 열어 Summary 탭과 Chart 탭으로 분석하는 플로우gc.logGC 로그 파일파일 열기GCViewer로컬 데스크톱오픈소스, 무료외부 전송 없음Summary 탭 — 핵심 수치Throughput (기준 >= 95%)Avg / Max Pause (기준 <= 200ms)Full GC 횟수 (기준 <= 3회)Chart 탭 — 패턴 시각화Heap 톱니 패턴 (정상) vs 우상향 (누수)GC Pause 막대 분포 확인

정상 패턴 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 비교

항목GCViewerGCEasy
방식로컬 데스크톱 앱웹 서비스
비용무료 (오픈소스)무료 플랜 있음 (제한적)
보안로그 외부 미전송외부 서버 업로드
분석 깊이기본 시각화 중심AI 기반 권고사항 포함
API 지원없음REST API 지원
지원 GCG1GC, 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 튜닝 워크플로우

GC 튜닝 워크플로우GC 로그 활성화 → 수집 → 분석 → 증상별 대응 → 재검증까지의 단계별 워크플로우GC 로그 활성화-Xlog:gc* 항상 켜두기로그 수집최소 1~2시간 운영 트래픽분석 도구로 진단GCViewer (로컬) / GCEasy (웹)Throughput 낮음< 95%Heap 크기 증가Max Pause 김> 500msMaxGCPause 설정Full GC 빈발> 3회IHOP % 낮추기Heap 우상향GC 후 미수거Heap Dump 분석동일 부하 조건에서 튜닝 전후 재검증

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 8Parallel GC
Java 9~14G1GC
Java 15+G1GC (ZGC/Shenandoah는 명시 필요)
Java 21G1GC (ZGC 프로덕션 준비 완료)

👉 ZGC vs G1GC — Off-Heap 메모리 해제 차이 완전 정리

👉 JDK Flight Recorder + JDK Mission Control 실전 — 운영 중 JVM 프로파일링


11. 마무리

이번 글을 세 줄로 정리합니다.

  1. GC 로그는 -Xlog:gc*로 항상 활성화해 두어야 레이턴시 급등의 원인을 사후에 추적할 수 있습니다.
  2. GCViewer(로컬 보안 환경)와 GCEasy(빠른 자동 리포트)를 상황에 따라 조합해 Throughput, Max Pause, Full GC 횟수를 핵심 지표로 분석합니다.
  3. 튜닝은 반드시 동일 부하 조건에서 전후 비교로 검증하고, Heap 우상향 패턴이 보이면 GC 튜닝보다 Heap Dump 분석을 먼저 진행하세요.

다음 글에서는 JDK Flight Recorder(JFR) + JDK Mission Control(JMC) 로 GC 로그보다 더 깊이, 운영 서버에서 실시간으로 메모리 할당 핫스팟과 스레드 병목을 찾는 방법을 다루겠습니다.

Y

yshyuk

Java 백엔드 개발자 | Spring, AWS, DevOps
2020년부터 Java/Spring boot 서버 개발자로 일하면서, 인프라(AWS/NCP), DevOps 업무를 병행하였고, 현재는 OpenSource 기여에도 관심을 가지고 있습니다.

조회수: 1