개요

JVM 메모리 관리와 가비지 컬렉션(GC) 최적화는 고성능 Java 애플리케이션의 핵심입니다. 잘못된 GC 설정은 stop-the-world 이벤트로 인해 애플리케이션 지연을 유발하고, 메모리 누수는 OOM(Out of Memory) 에러로 이어집니다.

이 가이드에서는 JVM 메모리 구조, 주요 GC 알고리즘, 실전 튜닝 기법을 다룹니다.

JVM 메모리 구조 심층 분석

JVM 힙 메모리는 Young Generation과 Old Generation으로 나뉘며, Young Generation은 다시 Eden, Survivor0, Survivor1 영역으로 구성됩니다.

메모리 영역별 역할

Heap Memory
├── Young Generation (1/3 of heap)
│   ├── Eden Space (80%)        # 새 객체 할당
│   ├── Survivor 0 (10%)        # Minor GC 생존 객체
│   └── Survivor 1 (10%)        # Minor GC 생존 객체
└── Old Generation (2/3 of heap)  # 오래 살아남은 객체

Non-Heap Memory
├── Metaspace (unlimited)       # 클래스 메타데이터
├── Code Cache                  # JIT 컴파일 코드
└── Thread Stacks               # 스레드별 스택

핵심 JVM 옵션

# 힙 메모리 설정
-Xms4g              # 초기 힙 크기
-Xmx4g              # 최대 힙 크기 (Xms와 동일하게 설정 권장)
-Xmn1g              # Young Generation 크기

# Metaspace 설정
-XX:MetaspaceSize=256m
-XX:MaxMetaspaceSize=512m

# GC 로깅 (Java 11+)
-Xlog:gc*:file=gc.log:time,uptime,level,tags

# GC 상세 정보
-XX:+PrintGCDetails
-XX:+PrintGCDateStamps
-XX:+PrintHeapAtGC

주요 GC 알고리즘 비교

1. G1 GC (기본 GC, Java 9+)

G1(Garbage First)은 대용량 힙에 최적화된 GC입니다. 힙을 동일한 크기의 리전으로 나누고, 가비지가 많은 리전부터 수거합니다.

# G1 GC 설정
-XX:+UseG1GC
-XX:MaxGCPauseMillis=200        # 목표 최대 정지 시간
-XX:G1HeapRegionSize=16m        # 리전 크기 (1~32MB)
-XX:InitiatingHeapOccupancyPercent=45  # Old Gen GC 시작 임계값

# 사용 사례
java -Xms8g -Xmx8g      -XX:+UseG1GC      -XX:MaxGCPauseMillis=200      -XX:+ParallelRefProcEnabled      -jar application.jar

장점: 예측 가능한 정지 시간, 대용량 힙 지원
단점: CPU 오버헤드가 높음
적합한 경우: 4GB 이상 힙, 지연 시간에 민감한 웹 애플리케이션

2. ZGC (Ultra-Low Latency)

ZGC는 10ms 미만의 초저지연을 목표로 하며, 힙 크기에 무관하게 일정한 정지 시간을 유지합니다.

# ZGC 설정 (Java 15+에서 프로덕션 준비)
-XX:+UseZGC
-XX:ConcGCThreads=4             # 동시 GC 스레드 수
-XX:ZAllocationSpikeTolerance=2  # 할당 급증 허용치

# 사용 예제
java -Xms16g -Xmx16g      -XX:+UseZGC      -XX:ConcGCThreads=4      -Xlog:gc:file=zgc.log      -jar application.jar

장점: 10ms 미만 정지 시간, 대용량 힙(수 TB) 지원
단점: Java 15+ 필요, 메모리 오버헤드
적합한 경우: 실시간 트레이딩, 게임 서버, 대용량 데이터 처리

3. Shenandoah GC

Red Hat이 개발한 저지연 GC로, ZGC와 유사하지만 Java 8 백포트를 지원합니다.

# Shenandoah 설정
-XX:+UseShenandoahGC
-XX:ShenandoahGCHeuristics=adaptive  # adaptive, static, compact

java -Xms8g -Xmx8g      -XX:+UseShenandoahGC      -Xlog:gc:file=shenandoah.log      -jar application.jar

4. Parallel GC (처리량 중심)

멀티 CPU를 활용한 처리량 최적화 GC입니다. 배치 작업에 적합합니다.

# Parallel GC 설정
-XX:+UseParallelGC
-XX:ParallelGCThreads=8         # GC 스레드 수
-XX:MaxGCPauseMillis=500        # 최대 정지 시간 (힌트)

java -Xms4g -Xmx4g      -XX:+UseParallelGC      -XX:ParallelGCThreads=8      -jar batch-job.jar

실전 튜닝 시나리오

시나리오 1: Full GC 빈번 발생

증상: Old Generation이 빠르게 차서 Full GC가 자주 발생

원인 분석:

# GC 로그 분석
jstat -gcutil  1000

# 출력 예시
S0     S1     E      O      M     CCS    YGC     YGCT    FGC    FGCT     GCT
0.00  99.99  50.00  99.00  95.00  90.00  1500   15.000  100   50.000   65.000

# Old Generation(O) 사용률이 99% → 문제!

해결책:

# Old Generation 크기 증가
-Xms8g -Xmx8g -Xmn2g  # Young 2GB, Old 6GB

# 또는 객체가 Old로 승격되는 임계값 조정
-XX:MaxTenuringThreshold=15  # 기본값은 15
-XX:TargetSurvivorRatio=90   # Survivor 공간 활용률

시나리오 2: 긴 GC 정지 시간

증상: GC 정지 시간이 수 초에 달함

해결책:

# G1 GC로 전환하고 목표 정지 시간 설정
-XX:+UseG1GC
-XX:MaxGCPauseMillis=100  # 100ms 목표

# 또는 ZGC 사용 (Java 15+)
-XX:+UseZGC

시나리오 3: 메모리 누수 탐지

힙 덤프 생성:

# OOM 발생 시 자동 힙 덤프
-XX:+HeapDumpOnOutOfMemoryError
-XX:HeapDumpPath=/var/log/heapdump.hprof

# 수동 힙 덤프
jmap -dump:live,format=b,file=heap.hprof 

# 힙 덤프 분석 (Eclipse MAT 사용)
# 1. Leak Suspects Report 실행
# 2. Dominator Tree에서 큰 객체 확인
# 3. GC Root로 역추적

모니터링 도구

# 실시간 GC 통계
jstat -gc  1000 10

# 힙 히스토그램
jmap -histo:live  | head -20

# 스레드 덤프
jstack  > threads.txt

# Java Mission Control (JFR)
java -XX:+FlightRecorder      -XX:StartFlightRecording=duration=60s,filename=recording.jfr      -jar application.jar

활용 팁

  • Xms와 Xmx 동일하게 설정: 힙 크기 변동으로 인한 오버헤드를 제거합니다.
  • Young/Old 비율: 일반적으로 Young 25%, Old 75%가 적절합니다.
  • GC 로그 필수: 프로덕션 환경에서도 GC 로그를 항상 활성화하세요.
  • 점진적 튜닝: 한 번에 하나의 옵션만 변경하여 효과를 측정합니다.
  • 부하 테스트: 프로덕션 부하를 시뮬레이션하여 GC 동작을 검증합니다.
  • 컨테이너 환경: Java 10+는 컨테이너 메모리 제한을 자동 인식합니다 (-XX:+UseContainerSupport).

마무리

JVM 튜닝은 과학이자 예술입니다. 애플리케이션 특성, 트래픽 패턴, 하드웨어 스펙에 따라 최적의 설정이 달라집니다. GC 알고리즘 선택은 지연 시간과 처리량 사이의 트레이드오프이며, 만능 해결책은 없습니다.

G1 GC로 시작하여 문제가 발생하면 ZGC나 Shenandoah로 전환하는 것이 안전한 접근법입니다. 지속적인 모니터링과 프로파일링을 통해 점진적으로 개선하세요.

JVM의 깊은 이해는 시니어 개발자와 주니어 개발자를 구분하는 핵심 역량입니다. 이 가이드를 기반으로 여러분의 애플리케이션을 최적화해보시기 바랍니다.