Java heap dump 를 떴을 때 가장 어려운 부분은, 지금 보고 있는 것이 진짜 leak 인지, 일시적인 workload burst 인지, 아니면 backlog 때문에 객체가 예상보다 오래 살아남은 상황인지를 구분하는 것입니다. heap dump 는 실제 object graph 를 보여 주지만, 해석은 여전히 필요합니다.
짧게 말하면 핵심은 이것입니다. raw object count 보다 retained memory 부터 봐야 합니다. instance 수가 가장 많은 class 가 항상 진짜 culprit 는 아닙니다. 어떤 object graph 가 retained heap 을 지배하는지, 그리고 왜 아직 reachable 한지가 더 중요합니다.
Java 전체 증상 분기부터 다시 보고 싶다면 Java Troubleshooting Guide 로 먼저 돌아가도 좋습니다.
object count 보다 retained memory 부터 본다
instance 가 많다고 자동으로 문제가 되는 것은 아닙니다.
더 중요한 것은 아래입니다.
- retained size
- dominator 관계
- reference chain
- 그 객체가 아직 살아 있어야 하는지 여부
그래서 단순 histogram 보다 dominator 와 GC root 까지의 경로가 훨씬 유용한 경우가 많습니다.
heap dump 는 어떤 질문에 가장 유용한가
heap dump 는 보통 이런 질문에 답할 때 가장 가치가 큽니다.
- 지금 가장 많은 메모리를 붙잡고 있는 것은 무엇인가
- 이 성장이 cache, backlog, leaked reference 중 어디에서 오는가
- 객체가 예상보다 오래 살아남고 있는가
- 이 현상이 OOM 이나 긴 GC pause 와 이어지는가
적절한 시점에 뜬 dump 하나가 추측을 concrete retention path 로 바꿔 주기도 합니다.
흔한 원인
1. 큰 collection 이 계속 커진다
가장 자주 보이는 패턴 중 하나입니다.
map, list, queue, cache 는 아래 상황에서 heap 을 지배하기 쉽습니다.
- eviction 이 없다
- 소비 속도가 느리다
- 요청별 데이터가 중복 저장된다
- 각 entry 가 생각보다 크다
이건 고전적인 leak 이라기보다 unbounded retention 인 경우도 많습니다.
2. backlog 가 request data 를 붙잡고 있다
queued work 가 payload, context, response, closure 를 예상보다 오래 살아 있게 만들 수 있습니다.
시스템이 뒤처지는 중이라면, heap dump 는 원래 성능 병목보다 queue 증상을 더 또렷하게 보여 줄 수도 있습니다.
3. reference chain 이 cleanup 을 막는다
수거되어야 할 객체가 아래를 통해 계속 reachable 할 수 있습니다.
- singleton
- static holder
- thread local
- listener registry
- 실질적 만료가 없는 cache
이럴 때 GC root 까지의 path 가 특히 중요해집니다.
4. snapshot 을 뜬 시점이 적절하지 않다
짧은 burst 중에 잡은 dump 는 장기 leak 이 아니라 일시 압력을 보여 줄 수 있습니다.
그래서 dump 하나도 유용하지만, traffic 타이밍과 여러 dump 비교가 더 좋은 경우가 많습니다.
5. 큰 retained graph 는 이야기의 일부일 뿐이다
dump 에서 큰 retained structure 가 보여도, incident 의 출발점은 다른 곳일 수 있습니다.
- queue buildup
- 느린 downstream dependency
- traffic spike
- retry storm
즉 dump 는 도움이 되지만, 운영 맥락 속에서 읽어야 더 정확합니다.
실전 점검 순서
1. 가장 큰 retained object 와 dominator 를 찾는다
instance 수가 많은 것보다 retained heap 을 지배하는 것을 먼저 보세요.
이 단계가 attention 을 어디에 써야 할지 정해 줍니다.
2. 그 객체를 살려 두는 reference path 를 본다
질문은 이렇습니다.
- 어떤 object 가 이 graph 를 소유하고 있는가
- 그 owner 는 지금도 살아 있어야 하는가
- 의도된 reference 인가, accidental reference 인가
이 단계에서 “메모리가 높다” 가 실제 코드 경로 문제로 바뀌는 경우가 많습니다.
3. cache, queue, large collection 이 기대 범위를 벗어났는지 비교한다
크다는 사실만 보면 안 되고, 설계 기대치 대비 큰지를 물어야 합니다.
예를 들면:
- 현재 load 에서 queue size 가 정상인가
- cache 에 bound 가 있는가
- 배포 이후 collection 이 갑자기 커졌는가
4. heap dump 시점과 traffic 또는 deployment 변화를 비교한다
같은 retained graph 도 언제 dump 를 떴는지에 따라 의미가 달라집니다.
짧은 burst 중의 snapshot 과, 몇 시간에 걸친 steady growth 뒤 snapshot 은 같은 식으로 읽으면 안 됩니다.
5. dump 가 retention 을 확인해 주면 pause 또는 OOM 증상과 다시 연결한다
같은 retained graph 가 GC pause 를 늘리거나 서비스를 failure 로 밀고 있다면, 그 증상과 heap evidence 를 연결해서 봐야 합니다.
예시: leak 처럼 보이는 queue retention
jcmd <pid> GC.heap_dump heap.hprof
예를 들어 dump 에서 thread pool queue 안의 task 들이 많은 request payload object 를 붙잡고 있다면, 처음엔 고전적인 memory leak 처럼 보일 수 있습니다. 하지만 실제 문제는 worker 가 느려져 backlog 가 생겼고, 그 때문에 객체가 오래 살아남은 것일 수 있습니다.
그래서 heap dump 는 queue 와 throughput 신호와 함께 읽는 편이 좋습니다.
retained graph 를 찾은 뒤 무엇을 바꾸면 좋나
cache 나 collection 이 unbounded 라면
real limit, eviction, lifecycle control 이 필요합니다.
backlog 가 데이터를 너무 많이 붙잡는다면
queue buildup 을 줄이고, task 가 오래 기다리게 만드는 throughput bottleneck 을 먼저 고쳐야 합니다.
thread local 이나 static reference 가 데이터를 잡고 있다면
cleanup 과 ownership 경계를 더 명확하게 해야 합니다.
dump 가 짧은 burst 를 반영한 것이라면
바로 leak 으로 단정하지 말고 이후 snapshot 과 비교해야 합니다.
같은 graph 가 dump 마다 계속 커진다면
강한 leak 또는 retention 신호로 보고 owner path 를 직접 추적해야 합니다.
장애 중에 던져볼 질문
이 질문이 꽤 유용합니다.
가장 많은 retained memory 를 가진 object graph 는 무엇이고, 그 ownership 이 지금 request 또는 task lifecycle 상 여전히 존재해야 맞는가?
이 질문이 class count 만 보는 것보다 훨씬 빨리 fix 로 이어집니다.
FAQ
Q. object count 가 크면 leak 인가
항상은 아닙니다. retained size 와 reachability 가 count 보다 더 중요합니다.
Q. dump 를 여러 번 떠야 하나
같은 retained graph 가 시간에 따라 계속 커지는지 비교하려면 그렇습니다.
Q. 가장 빠른 첫 단계는 무엇인가
가장 큰 dominator 와 그 reference path 를 찾는 것입니다.
Q. heap dump 가 true leak 이 아니라 backlog 를 보여 줄 수도 있나
그렇습니다. queued work 는 classic forever leak 이 아니어도 실제 memory pressure 를 크게 만들 수 있습니다.
Read Next
- retained object 가 pause time 도 늘리고 있다면 Java GC Pauses Too Long 을 이어서 보세요.
- 같은 retained graph 가 failure 로 이어지고 있다면 Java OutOfMemoryError 와 같이 보세요.
- owner path 가 queue growth 쪽으로 보인다면 Java Thread Pool Queue Keeps Growing 를 확인해 보세요.
- 전체 Java 분기 지도를 다시 보려면 Java Troubleshooting Guide 로 돌아가면 됩니다.
Related Posts
- Java GC Pauses Too Long
- Java OutOfMemoryError
- Java Thread Pool Queue Keeps Growing
- Java Troubleshooting Guide
Sources:
심사 대기 중에는 광고 대신 관련 가이드를 먼저 보여줍니다.
먼저 읽어볼 가이드
검색 유입이 많은 핵심 글부터 이어서 보세요.
- 미들웨어 트러블슈팅 가이드: Redis vs RabbitMQ vs Kafka 개발자를 위한 미들웨어 트러블슈팅 허브 글입니다. Redis, RabbitMQ, Kafka 중 어떤 증상부터 먼저 봐야 하는지와 어떤 문제 패턴이 각 시스템에 가까운지 정리합니다.
- Kubernetes CrashLoopBackOff: 먼저 볼 것들 startup failure, probe, config, resource limit 관점에서 CrashLoopBackOff를 어떻게 나눠서 봐야 하는지 정리한 가이드입니다.
- Kafka consumer lag가 계속 늘 때: 트러블슈팅 가이드 Kafka consumer lag가 계속 늘어날 때 무엇부터 봐야 하는지 정리합니다. poll 주기, 처리 속도, rebalance, consumer 설정까지 실전 기준으로 다룹니다.
- Kafka Rebalancing Too Often 가이드 Kafka consumer group에서 rebalance가 너무 자주 일어날 때 membership flapping, poll timing, protocol, assignment churn을 어떤 순서로 봐야 하는지 설명하는 실전 가이드입니다.
- Docker container가 계속 재시작될 때: 먼저 확인할 것들 exit code, command failure, environment mistake, health check 관점에서 Docker restart loop를 푸는 실전 가이드입니다.
심사 대기 중에는 광고 대신 관련 가이드를 먼저 보여줍니다.