JVM 성능 문제는 어디서 오는가
GC 로그 분석부터 Heap Dump, Flame Graph, JVM 튜닝, Actuator 메트릭까지 — Java 애플리케이션 성능 저하의 원인을 추적하는 계층적 진단 체계를 정리한다.
- 01 성능 테스트는 왜 유형이 다른가
- 02 성능 테스트 도구는 왜 아키텍처가 결과를 결정하는가
- 03 성능 병목은 어디서 오는가 — USE 방법론부터 스레드 덤프까지
- 04 JVM 성능 문제는 어디서 오는가
- 05 DB 성능 튜닝은 어디서 시작해야 하는가
- 06 성능 튜닝은 과학이다 — 하나씩, 측정하고, 번역하라
- 07 성능 테스트가 거짓말을 하는 이유
Java 애플리케이션이 갑자기 느려졌다. 로그에는 아무것도 없다. CPU는 정상이다. 그런데 p99 응답시간이 500ms를 넘는다. 이 침묵 속의 지연을 어떻게 추적하는가?
층위를 이해하는 것이 먼저다
JVM 성능 문제에는 층위가 있다. GC 로그는 메모리 관련 지연을 드러내지만, Lock 경합이나 I/O 대기는 보여주지 않는다. JFR은 JVM 전체 이벤트를 기록하지만, 메서드 수준의 CPU 병목은 Flame Graph가 더 직관적이다. Heap Dump는 메모리 누수의 정확한 원인을 보유 체인으로 추적한다. 그리고 Actuator는 이 모든 것을 실시간으로 관찰하게 해준다.
각 도구가 독립적으로 존재하는 것이 아니라, 진단 계층을 형성한다. 증상 → 원인 → 근거 순으로 좁혀가는 과정이 핵심이다.
GC 로그 — 침묵의 지연을 꺼내는 첫 번째 도구
응답시간 스파이크의 상당수는 GC의 Stop-The-World(STW) 때문이다. GC 로그가 없으면 이 원인은 영원히 추측으로 남는다.
Java 9+에서 권장 옵션은 다음과 같다.
java -Xmx2g -Xms2g \
-Xlog:gc*:file=logs/gc.log:time,uptime,level,tags:filecount=10,filesize=100m \
MyApplication
로그 한 줄의 구조를 읽을 수 있어야 한다.
[0.187s][info][gc] GC(1) Pause Young (Concurrent Start) 31M->15M(2048M) 34.123ms
마지막 숫자 34.123ms가 STW 시간이다. 이 숫자가 수백 ms라면 사용자가 느낀다. 수 초라면 타임아웃이 발생한다.
GC 종류별 위험도는 다르다. Minor GC(10100ms)는 대체로 무시할 수 있다. Major GC(100ms수 초)는 주의가 필요하다. Full GC(수 초~수십 초)는 정상 운영에서 발생하면 안 된다 — 발생 자체가 메모리 누수나 힙 부족의 신호다.
Full GC가 시간당 1회 이상 발생한다면, GC 튜닝보다 먼저 메모리 누수를 의심하라. GC 로그에서 Young GC 직후의 Heap 사용량이 계속 증가한다면, Old Gen이 해제되지 않는 객체로 차고 있다는 의미다.
JFR과 async-profiler — GC 너머를 본다
GC STW가 50ms인데 응답시간이 500ms라면, 나머지 450ms는 어디서 오는가? GC 로그는 이 질문에 답하지 못한다.
Java Flight Recorder(JFR) 는 CPU, 메모리 할당, Lock 경합, I/O 대기를 한 번에 기록한다. 오버헤드 1% 미만으로 프로덕션에서 상시 활성화해도 안전하다.
# 실행 중인 JVM에 30초 녹화 시작
jcmd 12345 JFR.start duration=30s filename=/tmp/recording.jfr settings=profile
JFR이 “무엇이 일어났는가”를 기록한다면, async-profiler는 “어디서 CPU를 쓰는가”를 Flame Graph로 보여준다.
./profiler.sh -d 60 -f flamegraph.html -e cpu 12345
Flame Graph의 독해법은 단순하다 — 가로 길이가 곧 CPU 시간이다. 가장 넓은 스택 프레임이 병목이다. executeSQL()이 전체 너비의 40%를 차지한다면, 그것이 최우선 최적화 대상이다.
Wall-Clock 프로파일링(-e wall)과 CPU 프로파일링의 차이도 중요하다. CPU 프로파일에서 databaseQuery()가 5%인데 Wall-Clock에서 70%라면, 그 메서드가 I/O 대기 중이라는 뜻이다 — CPU가 아니라 데이터베이스가 병목이다.
Heap Dump — 메모리 누수의 보유 체인 추적
GC 로그에서 Full GC 빈발을 확인했다면, 다음 단계는 Heap Dump다. 현재 JVM 메모리의 스냅샷을 찍어 MAT(Memory Analyzer Tool)로 분석하면, 어떤 객체가 왜 해제되지 않는지를 GC Root에서부터의 경로로 추적할 수 있다.
jmap -dump:format=b,file=heap.hprof 12345
MAT의 “Leak Suspects” 리포트는 전체 힙의 대부분을 점유하는 객체 집합을 찾아낸다. 핵심은 Retention Path — static UserCache → sessions HashMap → Session 객체 → byte[] userData 같은 보유 체인이다. 이 체인이 끊기지 않는 한 GC는 해당 메모리를 회수하지 못한다.
시간 간격을 두고 두 개의 Heap Dump를 비교하면 누수 여부가 명확해진다. Session 객체가 매시간 2,000개씩 증가한다면, 캐시 eviction 로직이 없거나 TTL이 설정되지 않은 것이다.
GC 알고리즘 선택 — 트레이드오프
원인을 파악했다면 이제 튜닝이다. GC 알고리즘 선택이 가장 큰 변수다.
G1GC: 힙 440GB 범위, STW 50200ms, CPU 오버헤드 낮음. 대부분의 웹 백엔드에 적합하다. -XX:MaxGCPauseMillis=200으로 목표 STW를 설정할 수 있다.
ZGC: STW < 1ms 보장, 대용량 힙(8GB+) 최적화, 동시 GC. 대신 CPU 오버헤드 약 25% 추가. p99 < 50ms가 필요한 금융 시스템이나 트레이딩 플랫폼에 선택한다.
힙 크기 설정에서 흔한 실수는 -Xms와 -Xmx를 다르게 주는 것이다. 동적 리사이징이 발생할 때마다 STW가 추가로 수백 ms 발생한다. -Xms와 -Xmx는 반드시 같아야 한다.
# ❌ 동적 리사이징 유발
java -Xms512m -Xmx8g MyApplication
# ✅ 고정 힙, 리사이징 비용 제거
java -Xms8g -Xmx8g -XX:+UseZGC MyApplication
컨테이너 환경에서는 -XX:+UseContainerSupport를 명시해야 JVM이 컨테이너 메모리 제한을 올바르게 인식한다. 그렇지 않으면 호스트 메모리 기준으로 힙을 할당해 컨테이너 OOM이 발생할 수 있다.
Actuator 메트릭 — 실시간 관찰
진단 도구들은 사후 분석에 강하다. 실시간 감지는 Spring Boot Actuator + Prometheus + Grafana 스택이 담당한다.
핵심 메트릭 몇 가지만 모니터링해도 대부분의 문제를 조기에 잡을 수 있다.
| 메트릭 | 경고 임계값 | 의미 |
|---|---|---|
| Heap 사용률 | > 80% | GC 압박 증가 |
| GC Pause Time | > 200ms | 사용자 체감 지연 |
| HTTP p99 Latency | > 500ms | SLA 위협 |
| Active DB Connections | > 80% | 커넥션 풀 포화 |
| Pending DB Requests | > 0 | 커넥션 부족, 즉시 조치 |
hikaricp.connections.pending > 0은 즉시 알람이 필요하다. 이 메트릭이 양수라는 것은 요청이 커넥션을 얻지 못해 대기 중이라는 의미이고, 이는 스레드 누적 → 힙 증가 → GC 빈발 → 응답시간 악화의 연쇄를 시작하는 신호다.
정리
- GC 로그로 STW를 측정하고, Full GC 빈발은 메모리 누수의 징조로 해석한다.
- JFR과 async-profiler Flame Graph는 GC 너머의 CPU, Lock, I/O 병목을 찾는다.
- Heap Dump + MAT는 메모리 누수의 정확한 보유 체인을 추적한다.
- GC 알고리즘(G1GC vs ZGC)과 힙 고정 설정은 진단 이후의 처방이다.
- Actuator 메트릭은 이 모든 것을 실시간으로 관찰하고 조기 경보를 가능하게 한다.
침묵하는 지연을 추적하는 것은 단일 도구의 문제가 아니라, 계층적 진단 체계를 얼마나 익히고 있느냐의 문제다.