티스토리 뷰
Stop-the-World와 메모리 관리 - Java GC의 이해와 실전 튜닝 4편: GC 로그인가, 힙 덤프인가 - 상황별 진단 도구 선택과 분석 워크플로우
ebson 2026. 4. 9. 23:15들어가며
이전 글에서는 Serial GC부터 G1GC까지 Java GC 알고리즘의 진화 과정을 정리했습니다. G1GC의 내부 구조를 이해하는 것이 실무에서의 문제 진단과 직결된다고 마무리했는데, 이번 글에서는 그 진단을 어떻게 수행하는지를 다루겠습니다.
운영 환경에서 GC 관련 문제가 발생했을 때, 개발자가 사용할 수 있는 진단 도구는 크게 두 가지입니다. GC 로그 분석과 힙 덤프 분석입니다. 경험이 쌓이기 전에는 이 둘의 차이가 모호하게 느껴질 수 있습니다. 둘 다 메모리 문제를 다루는 도구이고, 둘 다 JVM에서 데이터를 추출한다는 공통점이 있기 때문입니다.
하지만 이 두 도구는 서로 다른 질문에 답합니다. GC 로그는 "GC가 어떻게 동작하고 있는가"를 보여주고, 힙 덤프는 "힙 안에 무엇이 있는가"를 보여줍니다. 문제의 성격을 먼저 판별한 뒤 적합한 도구를 선택해야 하며, 실무에서는 두 도구를 상호 보완적으로 사용하는 것이 일반적입니다.
이번 글에서는 각 도구가 필요한 상황, 주요 분석 도구의 특성, 그리고 실무에서 두 방법을 조합하여 문제를 진단하는 워크플로우를 정리해 보겠습니다.
GC 로그 — GC가 어떻게 동작하고 있는가
GC 로그는 JVM이 GC를 수행할 때마다 기록하는 이벤트 로그입니다. 각 GC 이벤트의 발생 시각, 종류(Young GC, Mixed GC, Full GC 등), 소요 시간, 회수 전후의 힙 사용량 등이 시계열로 기록됩니다. GC 로그의 핵심 가치는 GC 동작의 패턴과 추세를 파악할 수 있다는 데 있습니다.
GC 로그를 봐야 하는 상황
GC 로그 분석이 필요한 대표적인 상황들을 정리하면 다음과 같습니다.
가장 흔한 경우는 GC 빈도의 증가입니다. 단위 시간당 GC 발생 횟수가 평소 대비 급증했다면, 객체 할당률(Allocation Rate)이 높아졌거나 힙 크기가 현재 워크로드 대비 부족하다는 신호입니다. 배포 이후 GC 빈도가 갑자기 늘었다면 새로 추가된 코드에서 불필요한 객체를 과도하게 생성하고 있을 가능성을 먼저 살펴봐야 합니다.
Pause Time의 급등도 GC 로그를 통해 확인합니다. 개별 GC 이벤트의 STW 시간이 허용 범위를 넘어서면 애플리케이션의 응답 시간에 직접적인 영향을 미칩니다. 특히 p99 지연 시간이 갑자기 튀는 현상의 원인이 GC인 경우가 많으며, GC 로그에서 해당 시간대의 pause time을 확인하면 바로 판별할 수 있습니다.
Throughput 저하는 전체 실행 시간 중 GC에 소비되는 비율이 증가하는 현상입니다. Oracle의 GC 튜닝 가이드에서는 GC 오버헤드가 5%를 초과하면 주의가 필요하고, 10% 이상이면 본격적인 튜닝이 필요하다고 안내하고 있습니다. GC 로그 분석 도구들은 이 throughput 비율을 자동으로 계산해 줍니다.
Promotion Rate의 이상은 Young Generation에서 Old Generation으로 승격되는 데이터량이 비정상적으로 높은 상황입니다. 3편에서 다룬 세대별 가설에 따르면, 대부분의 객체는 Young Generation에서 죽어야 합니다. 그런데 승격률이 높다면 객체의 수명이 예상보다 길거나, Young Generation이 너무 작아서 객체가 충분히 걸러지기 전에 Old Generation으로 넘어가는 것(Premature Promotion)일 수 있습니다.
Full GC의 반복 발생은 가장 심각한 신호입니다. Old Generation이 반복적으로 가득 차서 Full GC가 발생하고, Full GC 이후에도 Old Generation 사용량이 크게 줄지 않는다면 메모리 누수를 의심해야 합니다. 이 시점이 GC 로그에서 힙 덤프로 전환해야 하는 지점입니다.
GC 로그 활성화
JDK 9 이후부터는 Unified Logging(JEP 158, JEP 271)이 도입되어 GC 로그의 형식과 옵션이 통합되었습니다. 프로덕션 환경에서 권장되는 설정은 다음과 같습니다.
-Xlog:gc*:file=gc.log:time,uptime,level,tags:filecount=5,filesize=100m
이 설정은 GC 관련 모든 이벤트(gc*)를 파일(gc.log)에 기록하며, 타임스탬프와 업타임을 포함하고, 로그 파일이 100MB에 도달하면 로테이션하여 최대 5개 파일을 유지합니다. GC 로그 활성화에 따른 성능 영향은 무시할 수 있는 수준이므로, 프로덕션에서 상시 활성화하는 것이 권장됩니다. 문제가 발생한 뒤에 로그를 켜면 이미 발생한 이벤트는 확인할 수 없기 때문입니다.
JDK 8 이전에서는 -XX:+PrintGCDetails -XX:+PrintGCDateStamps -Xloggc:gc.log 형식을 사용합니다. Unified Logging과 형식이 다르므로 분석 도구 사용 시 JDK 버전을 확인해야 합니다.
힙 덤프 - 힙 안에 무엇이 있는가
힙 덤프(Heap Dump)는 특정 시점에 JVM 힙 메모리에 존재하는 모든 객체의 스냅샷입니다. 각 객체의 타입, 크기, 필드 값, 그리고 다른 객체와의 참조 관계가 모두 기록됩니다. GC 로그가 시간 축을 따라 GC 이벤트의 흐름을 보여준다면, 힙 덤프는 한 시점의 힙 상태를 단면으로 잘라 보여주는 것입니다.
힙 덤프를 봐야 하는 상황
힙 덤프가 필요한 가장 대표적인 상황은 OutOfMemoryError(OOM)의 발생입니다. OOM이 발생한 시점의 힙에 어떤 객체가 메모리를 점유하고 있는지를 확인해야 원인을 특정할 수 있습니다. JVM 옵션에 다음을 추가해 두면 OOM 발생 시 자동으로 힙 덤프가 생성됩니다.
-XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/path/to/dump
이 설정은 프로덕션 환경에서 항상 활성화해 두는 것이 좋습니다. OOM은 재현이 어려운 경우가 많아서, 발생 시점의 힙 덤프를 확보하지 못하면 원인 분석 자체가 불가능해질 수 있습니다.
메모리 누수가 의심되는 경우에도 힙 덤프가 필요합니다. GC 로그에서 Full GC 후에도 Old Generation 사용량이 줄지 않는 패턴을 확인했다면, 어떤 객체들이 회수되지 않고 누적되고 있는지를 힙 덤프를 통해 추적해야 합니다. GC 로그는 "메모리가 회수되지 않고 있다"는 사실을 알려주지만, "무엇이 메모리를 잡고 있는가"는 답해주지 않습니다. 이 질문에 답할 수 있는 것이 힙 덤프입니다.
특정 객체의 비정상적 점유율을 확인해야 할 때도 힙 덤프를 사용합니다. 모니터링 메트릭에서 힙 사용량이 예상보다 높게 나타나지만 OOM까지는 이르지 않는 경우, 어떤 타입의 객체가 얼마나 많이, 왜 살아있는지를 힙 덤프로 확인할 수 있습니다.
힙 덤프 생성 방법
실행 중인 JVM에서 힙 덤프를 생성하는 방법은 두 가지입니다. jmap을 사용하는 전통적인 방법과, JDK 7 이후 권장되는 jcmd를 사용하는 방법입니다.
# jmap 사용
jmap -dump:format=b,file=heapdump.hprof <pid>
# jcmd 사용 (권장)
jcmd <pid> GC.heap_dump /path/to/heapdump.hprof
힙 덤프 생성 시 주의할 점이 있습니다. 덤프 생성 과정에서 STW가 발생하며, 힙 크기에 비례하는 시간이 소요됩니다. 또한 생성되는 파일의 크기가 힙 크기에 비례하여 수 GB에 달할 수 있습니다. 8GB 힙의 덤프 파일은 8GB 가까이 될 수 있으므로, 디스크 여유 공간과 파일 전송 방안을 사전에 고려해야 합니다. 이런 이유로 힙 덤프는 프로덕션에서 상시적으로 생성하는 것이 아니라, 필요한 시점에 한정적으로 사용하는 도구입니다.
덤프를 생성하지 않고도 빠르게 힙 상태를 확인할 수 있는 방법도 있습니다. jmap -histo <pid>를 실행하면 클래스별 인스턴스 수와 메모리 점유량을 출력합니다. 전체 힙 덤프보다 훨씬 빠르게 실행되므로, "어떤 클래스의 객체가 비정상적으로 많은가"를 빠르게 파악하는 1차 확인 용도로 유용합니다.
두 도구의 비교 - 서로 다른 질문, 서로 다른 답
GC 로그와 힙 덤프의 차이를 명확히 정리하면, 상황에 따른 도구 선택이 수월해집니다.
분석 대상의 측면에서, GC 로그는 GC 이벤트의 시계열 데이터입니다. GC가 언제 발생했고, 얼마나 걸렸으며, 힙을 얼마나 회수했는지가 시간 순서대로 기록됩니다. 힙 덤프는 특정 시점의 힙 메모리 스냅샷입니다. 그 시점에 존재하는 모든 객체 인스턴스와 이들 사이의 참조 관계가 기록됩니다.
답하는 질문의 측면에서, GC 로그는 "GC가 얼마나 자주, 얼마나 오래 실행되고 있는가?"에 답합니다. 힙 덤프는 "힙 안에 무엇이, 왜 살아있는가?"에 답합니다. GC 로그에서 "Old Generation이 줄지 않는다"는 사실을 확인한 뒤, 힙 덤프에서 "어떤 객체가 Old Generation을 점유하고 있는가"를 확인하는 것이 자연스러운 흐름입니다.
생성 비용의 측면에서, GC 로그는 매우 낮은 비용으로 상시 활성화할 수 있습니다. Oracle 공식 문서에서도 프로덕션 환경에서의 상시 활성화를 권장하고 있으며, 성능 영향은 무시할 수 있는 수준입니다. 반면 힙 덤프는 생성 시 STW가 발생하고, 파일 크기가 힙 크기에 비례하기 때문에 비용이 높습니다. 프로덕션에서 불필요하게 힙 덤프를 생성하면 서비스에 영향을 줄 수 있습니다.
각 도구의 한계도 명확합니다. GC 로그는 어떤 객체가 문제인지 알려주지 않습니다. "메모리가 부족하다"는 것은 알 수 있지만, "왜 부족한가"는 답할 수 없습니다. 힙 덤프는 GC 동작의 시간적 패턴을 보여주지 않습니다. 특정 시점의 단면만 보여주므로, "GC가 점점 느려지고 있다"는 추세는 확인할 수 없습니다.
GC 로그 분석 도구
GCEasy
GCEasy는 GC 로그 파일을 업로드하면 시각화된 분석 보고서를 생성해 주는 온라인 서비스입니다. GC pause 시간의 분포, 힙 사용량 추이, throughput, 객체 할당률 등을 한눈에 파악할 수 있는 그래프와 요약 정보를 제공합니다.
GCEasy의 강점은 빠른 1차 분석입니다. GC 로그 파일을 브라우저에 드래그 앤 드롭하면 몇 초 안에 보고서가 생성되며, GC 동작의 전체적인 건강 상태를 직관적으로 파악할 수 있습니다. 문제가 있는 경우 "Key Performance Indicators" 섹션에서 경고를 표시해 주기도 합니다. 프로덕션 로그를 빠르게 훑어볼 때 유용한 도구입니다.
GCViewer
GCViewer는 오픈소스 기반의 오프라인 GC 로그 분석 도구입니다. GC 로그를 파싱하여 힙 사용량, pause time, throughput 등을 그래프로 시각화합니다. GCEasy와 달리 외부 서비스에 로그를 업로드할 필요가 없으므로, 보안 정책상 외부 전송이 제한되는 환경에서 유용합니다. 다만 해석은 분석자가 직접 수행해야 하며, GCEasy만큼의 자동화된 인사이트는 제공하지 않습니다.
JDK Flight Recorder(JFR)와 JDK Mission Control(JMC)
JFR은 OpenJDK에 내장된 저비용 프로파일링 프레임워크입니다. GC 이벤트뿐 아니라 스레드 활동, I/O 작업, 메모리 할당, 잠금 경합 등 JVM 전체의 이벤트를 기록합니다. JMC(JDK Mission Control)는 JFR이 기록한 데이터를 시각화하고 분석하는 GUI 도구입니다.
JFR의 핵심 이점은 프로덕션 환경에서 상시 활성화해도 성능 영향이 매우 작다는 것입니다. Oracle 공식 문서에서는 일반적으로 1% 미만의 오버헤드를 언급하고 있습니다. GC 로그가 GC 이벤트에 한정된 정보를 제공하는 반면, JFR은 GC 이벤트를 스레드 활동, 메모리 할당 패턴과 함께 종합적으로 분석할 수 있습니다. "이 시점에 GC가 오래 걸린 이유가 특정 스레드의 대량 할당 때문인가?" 같은 복합적인 질문에 답할 수 있는 도구입니다.
# JFR 기록 시작
jcmd <pid> JFR.start name=gc_recording duration=60s filename=gc.jfr
# 지속적 기록 (프로덕션 권장)
-XX:StartFlightRecording=disk=true,maxage=24h,maxsize=1g,dumponexit=true,filename=app.jfr
힙 덤프 분석 도구
Eclipse MAT (Memory Analyzer Tool)
Eclipse MAT는 대용량 힙 덤프 분석의 사실상 표준 도구입니다. 수 GB 크기의 힙 덤프도 분석할 수 있으며, 메모리 누수 원인을 추적하기 위한 핵심 기능들을 제공합니다.
MAT의 가장 유용한 기능 중 하나는 Leak Suspects Report입니다. 힙 덤프를 로드하면 자동으로 메모리를 가장 많이 점유하고 있는 의심 객체들을 식별하여 보고서로 제공합니다. 분석 경험이 많지 않은 경우에도 이 보고서를 출발점으로 삼으면 방향을 잡을 수 있습니다.
Dominator Tree는 객체 간의 "지배" 관계를 트리 구조로 보여줍니다. 어떤 객체가 GC에 의해 회수되면 함께 회수될 수 있는 객체들의 메모리 합계, 즉 Retained Size를 보여줍니다. Shallow Size(객체 자체의 크기)는 작지만 Retained Size가 큰 객체가 있다면, 그 객체가 참조 체인을 통해 대량의 메모리를 간접적으로 점유하고 있다는 뜻입니다. 메모리 누수의 원인을 추적할 때 이 Retained Size를 기준으로 정렬하면 효과적입니다.
OQL(Object Query Language)은 SQL과 유사한 문법으로 힙 덤프를 쿼리할 수 있는 기능입니다. 특정 클래스의 인스턴스를 필터링하거나, 특정 필드 값을 가진 객체를 검색하는 등 자유로운 분석이 가능합니다. 예를 들어 SELECT * FROM java.util.HashMap WHERE size > 10000과 같은 쿼리로 비정상적으로 큰 HashMap을 찾을 수 있습니다.
Path to GC Roots 기능은 특정 객체가 GC에 의해 회수되지 않는 이유를 추적합니다. 해당 객체에서 GC Root까지의 참조 체인을 보여주므로, 어떤 코드가 이 객체를 살려두고 있는지를 파악할 수 있습니다. 메모리 누수 분석에서 원인 코드를 특정하는 마지막 단계에 해당합니다.
VisualVM
VisualVM은 JDK 8까지 기본 번들로 포함되었던 모니터링 도구입니다. 힙 덤프 분석, 스레드 모니터링, CPU/메모리 프로파일링을 통합적으로 제공합니다. Eclipse MAT만큼 깊은 힙 분석 기능을 제공하지는 않지만, 실시간 모니터링과 경량 분석을 한 도구에서 처리할 수 있다는 점에서 초기 진단 용도로 유용합니다.
jmap과 jcmd — 커맨드라인 도구
힙 덤프 생성 외에도 jmap은 빠른 힙 상태 확인 용도로 활용할 수 있습니다. jmap -histo <pid>를 실행하면 전체 힙 덤프를 생성하지 않고도 클래스별 인스턴스 수와 바이트 크기를 출력합니다. "어떤 클래스의 인스턴스가 비정상적으로 많은가"를 빠르게 확인하는 1차 점검에 적합합니다.
jcmd는 JDK 7 이후에 도입된 다목적 진단 도구로, 힙 덤프 생성뿐 아니라 GC 실행(GC.run), VM 정보 확인(VM.info), JFR 제어 등 다양한 진단 명령을 제공합니다. jmap보다 안정적이고 기능이 풍부하여, Oracle 공식 문서에서도 jcmd 사용을 권장하고 있습니다.
실무 워크플로우 - 두 도구를 어떻게 조합하는가
GC 로그와 힙 덤프는 경쟁 관계가 아니라 상호 보완 관계입니다. 실무에서 메모리 관련 문제를 진단할 때는 일반적으로 다음과 같은 순서를 따릅니다.
이상 탐지에서 GC 로그 분석까지
문제의 시작은 대부분 모니터링 시스템에서의 이상 탐지입니다. 힙 사용률의 증가, 응답 시간의 급등, GC pause time의 상승 같은 메트릭 이상이 감지되면 가장 먼저 GC 로그를 확인합니다.
GC 로그에서 확인하는 항목은 GC 빈도, 개별 GC의 pause time, throughput, promotion rate 등입니다. 이 지표들을 종합하면 문제의 성격을 대략적으로 판별할 수 있습니다. GC 설정이 워크로드에 맞지 않는 문제인지, 아니면 메모리 누수가 의심되는 상황인지를 구분하는 것이 이 단계의 목표입니다.
GC 설정 문제인 경우
GC 로그에서 힙 크기가 워크로드 대비 부족하거나, pause time target이 부적절하게 설정되어 있는 것으로 판단되면, GC 파라미터 튜닝으로 해결할 수 있습니다. 이 경우 힙 덤프까지 갈 필요 없이 GC 로그 수준에서 문제가 해결됩니다. 구체적인 튜닝 전략은 다음 편에서 다루겠습니다.
메모리 누수가 의심되는 경우
GC 로그에서 Full GC가 반복되면서 Old Generation 사용량이 줄지 않는 패턴이 확인되면, 메모리 누수를 의심해야 합니다. 이 시점에서 힙 덤프를 생성하여 2차 진단을 수행합니다.
힙 덤프 분석은 Eclipse MAT를 기준으로, Leak Suspects Report에서 시작하는 것이 효율적입니다. MAT가 자동으로 식별한 의심 객체를 확인한 뒤, Dominator Tree에서 Retained Size가 큰 객체를 추적합니다. 원인 객체를 특정하면 Path to GC Roots 기능으로 해당 객체가 회수되지 않는 이유, 즉 어떤 참조 체인이 이 객체를 살려두고 있는지를 확인합니다. 이 참조 체인의 끝에 있는 코드가 메모리 누수의 원인입니다.
흔한 누수 원인으로는 static 컬렉션에 데이터가 무한히 축적되는 경우, 이벤트 리스너를 등록하고 해제하지 않는 경우, 캐시에 만료 정책이 설정되어 있지 않은 경우 등이 있습니다.
수정과 검증
원인을 파악하고 코드를 수정한 뒤에는 GC 로그를 다시 확인하여 개선 효과를 검증합니다. Full GC 빈도가 줄었는지, Old Generation 사용량의 증가 추세가 안정되었는지를 확인하는 것입니다. 이 단계에서 다시 GC 로그가 활용되므로, 진단의 시작과 끝이 모두 GC 로그임을 알 수 있습니다.
핵심 원칙
이 워크플로우에서 일관된 원칙이 있습니다. GC 로그는 항상 먼저 봅니다. 힙 덤프는 GC 로그만으로 원인을 특정할 수 없을 때 꺼내는 정밀 도구입니다. 프로덕션에서 GC 로그와 OOM 시 자동 힙 덤프 설정은 항상 활성화해 두되, 수동 힙 덤프 생성은 필요한 시점에만 수행합니다.
마치며
이번 글에서는 GC 문제 진단의 두 가지 핵심 도구 (GC 로그 분석과 힙 덤프 분석) 의 차이와 각각의 적합한 사용 상황, 그리고 실무에서 두 도구를 조합하는 워크플로우를 정리해 보았습니다.
글을 정리하면서 다시 느낀 점은, 이 두 도구를 적절히 활용하려면 결국 앞선 편들에서 다룬 GC의 동작 원리에 대한 이해가 전제되어야 한다는 것입니다. GC 로그에서 "Mixed GC가 자주 발생하고 있다"는 사실을 확인했을 때, 3편에서 다룬 G1GC의 Mixed GC 사이클을 이해하고 있다면 이것이 Old Region의 liveness가 높아졌다는 의미임을 바로 파악할 수 있습니다. 힙 덤프에서 Dominator Tree를 볼 때도, Heap의 세대별 구조와 객체 승격 메커니즘을 알고 있어야 분석 결과를 올바르게 해석할 수 있습니다.
다음 글에서는 이 시리즈의 마지막 편으로, GC 로그 분석에서 확인한 문제를 실제로 어떻게 해결하는지, 운영 환경에서의 워크로드 유형별 GC 튜닝 전략과 튜닝 결과를 검증하는 방법을 정리해 보겠습니다.
참고 출처
- Oracle. "Java Platform, Standard Edition Troubleshooting Guide, Release 21." — Chapter: Troubleshoot Memory Leaks.
- Oracle. "JDK Mission Control User's Guide."
- Oracle. "Java Platform, Standard Edition Java Flight Recorder Runtime Guide."
- Eclipse Memory Analyzer (MAT) Documentation — eclipse.dev/mat
- OpenJDK. "Unified JVM Logging" (JEP 158, JEP 271)
- Oracle. "Java Platform, Standard Edition HotSpot Virtual Machine Garbage Collection Tuning Guide, Release 21."
'STUDY' 카테고리의 다른 글
- Total
- Today
- Yesterday
- 캐시 성능 비교
- spring batch 5
- Hot Key 문제
- 백엔드 성능 설계
- 동시성처리
- DB 트랜잭션
- Java Performance
- Enum 기반 싱글톤
- Redis 성능 개선
- Cache Penetration
- 스레드 생명주기
- 백엔드 성능
- Redis vs DB
- 백엔드 성능 튜닝
- InterruptedException
- TTL 설계
- DB 인덱스 성능
- Double-Checked Locking
- 트래픽 처리
- mybatis
- Spring Batch
- Eager Initialization
- 캐시와 인덱스
- Initialization-on-Demand Holder Idiom
- 캐시 장애
- Redis 캐시 전략
- 트랜잭션 관리
- Cache Aside
- 백엔드 아키텍처
- Cache Avalanche
| 일 | 월 | 화 | 수 | 목 | 금 | 토 |
|---|---|---|---|---|---|---|
| 1 | 2 | 3 | 4 | 5 | 6 | |
| 7 | 8 | 9 | 10 | 11 | 12 | 13 |
| 14 | 15 | 16 | 17 | 18 | 19 | 20 |
| 21 | 22 | 23 | 24 | 25 | 26 | 27 |
| 28 | 29 | 30 |
