Java 에서 GC pause 가 길어질 때 가장 흔한 실수는 이걸 일반적인 JVM tuning 문제로만 보는 것입니다. 긴 pause 는 보통 allocation pressure, retention, 혹은 현재 workload 와 맞지 않는 heap 구조를 반영합니다. collector flag 를 너무 빨리 바꾸면 실제 원인을 해결하지 못한 채 잠깐만 가려 버릴 때가 많습니다.
짧게 말하면 핵심은 이것입니다. pause shape, allocation rate, old generation growth 를 같이 봐야 합니다. 아주 긴 stop-the-world pause 가 가끔 오는 경우와, 짧지만 잦은 pause 가 계속 오는 경우는 같은 병목을 뜻하지 않습니다.
Java 전체 증상 분기부터 다시 보고 싶다면 Java Troubleshooting Guide 로 돌아가도 좋습니다.
평균 latency 보다 pause 모양부터 본다
평균 latency 하나로는 실제 GC pattern 이 가려질 수 있습니다.
구분해야 할 것은 이렇습니다.
- 드물지만 매우 큰 pause
- 자주 반복되는 중간 크기 pause
- traffic burst 때만 커지는 pause
- old generation pressure 와 맞물리는 pause
이 모양이 churn, retention, sizing, workload shift 중 어느 쪽인지 방향을 잡아 줍니다.
긴 GC pause 는 운영에서 어떻게 보이나
보통 이런 증상과 같이 보입니다.
- request latency spike
- collection 구간의 CPU 상승
- GC 후에도 old generation 이 높게 남아 있음
- traffic burst 이후 recovery 가 느림
- 겉보기엔 충분해 보이는 heap 이 실제로는 pause 를 길게 만든다
즉 visible pause 는 investigation 을 시작하게 만드는 마지막 증상이지, 첫 번째 이상 신호가 아닌 경우가 많습니다.
흔한 원인
1. allocation churn 이 너무 크다
짧게 살 객체가 너무 많으면 collection 빈도가 높아지고 load 상황에서 pause spike 가 더 잘 보입니다.
특히 이런 경로가 그렇습니다.
- 높은 request fan-out
- 반복적인 object transformation
- 큰 temporary buffer
- serialization-heavy path
allocation rate 가 크게 오르면 빨리 죽는 객체도 pause 비용을 크게 만들 수 있습니다.
2. old generation retention 이 커진다
큰 구조가 너무 오래 살아 있으면 major collection 이 느려지고 회복도 어려워집니다.
대표적인 source 는:
- cache
- in-memory queue
- response aggregation
- long-lived collection
이건 단순 churn 이 아니라 retention 이 개입됐다는 강한 신호입니다.
3. heap sizing 이 현재 traffic 과 맞지 않는다
낮은 traffic 에서는 괜찮던 heap 도 아래 변화가 오면 pause 가 길어질 수 있습니다.
- request volume 증가
- 더 큰 payload
- concurrency 증가
- object lifetime 변화
그렇다고 바로 “heap 을 늘리자” 는 뜻은 아닙니다. 예전 sizing assumption 이 현재 운영 현실과 안 맞는다는 뜻에 가깝습니다.
4. 진짜 병목은 GC 바깥에 있다
CPU saturation, blocked thread, queue buildup, downstream slowdown 이 먼저 있고, GC 는 그 압력을 더 크게 보이게 하는 경우도 많습니다.
서비스가 이미 뒤처지고 있다면 GC 는 그 압력이 눈에 띄는 장소가 될 뿐입니다.
5. 큰 retained object 가 pause 를 비싸게 만든다
OOM 전이라도 oversized retained graph 가 있으면 pause time 이 충분히 길어질 수 있습니다.
그래서 GC incident 는 heap dump 분석과 바로 이어지는 경우가 많습니다.
실전 점검 순서
1. GC log 또는 pause metric 에서 frequency 와 worst-case spike 를 본다
평균 숫자 하나만 보면 안 됩니다.
질문은 이렇습니다.
- pause 가 얼마나 자주 일어나는가
- 최악의 pause 는 어느 정도인가
- 최근 pattern 이 바뀌었는가
2. allocation rate 와 traffic 변화를 비교한다
배포나 workload 변화 뒤 allocation rate 가 크게 올랐다면, churn 이 지배적인 원인일 수 있습니다.
이 단계가 collector behavior 와 application behavior 를 나눠 줍니다.
3. old generation growth 와 long-lived retention 을 본다
GC 후에도 old generation 이 계속 높다면, collector tuning 보다 retention 에 더 집중해야 합니다.
4. 현재 workload 와 heap sizing 을 비교한다
heap 은 예전 traffic profile 에는 맞았을 수 있습니다.
그래도 memory 가 생산적으로 쓰이는지 모른 채 바로 heap enlargement 로 가는 건 피하는 편이 좋습니다.
5. retention 이 의심되면 heap analysis 로 넘어간다
pause 가 retained structure 와 맞물린다면, 다음으로 유용한 artifact 는 random JVM flag 조정이 아니라 heap dump 인 경우가 많습니다.
예시: retained object 때문에 늘어나는 pause
Map<String, byte[]> cache = new HashMap<>();
cache.put(key, new byte[10_000_000]);
큰 retained object 나 반복적인 대형 allocation 은 heap 이 아직 완전히 차지 않았더라도 GC pause 를 충분히 길게 만들 수 있습니다.
즉 긴 pause 는 full OOM 이 와야만 심각해지는 것이 아닙니다.
패턴을 찾은 뒤 무엇을 바꾸면 좋나
churn 이 핵심이라면
hot path 의 needless allocation 과 object copying 을 줄여야 합니다.
retention 이 핵심이라면
오래 살아 있는 graph 를 추적하고, 너무 오래 남는 구조를 줄여야 합니다.
heap sizing 이 낡은 가정이라면
workload 와 object lifetime story 를 이해한 뒤 의도적으로 resize 해야 합니다.
queue 나 backlog 가 더 깊은 문제라면
GC 만 탓하지 말고 throughput collapse 를 먼저 고쳐야 합니다.
CPU spike 도 함께 오른다면
runtime pressure 와 memory pressure 를 하나의 incident 로 봐야 합니다.
장애 중에 던져볼 질문
이 질문이 꽤 유용합니다.
pause 가 긴 이유가 GC 를 너무 자주 해서인가, old data 가 너무 많이 남아서인가, 아니면 workload 가 heap 설계를 넘어섰기 때문인가?
이 질문이 “collector 를 바꿔야 하나?” 보다 훨씬 행동 가능한 답을 줍니다.
FAQ
Q. collector 부터 바꿔야 하나
allocation churn, retention, undersizing 중 무엇이 중심 문제인지 보기 전에는 권하지 않습니다.
Q. pause 가 길면 memory leak 인가
항상은 아닙니다. bursty allocation, 큰 heap, workload 변화 때문일 수도 있습니다.
Q. 가장 빠른 첫 단계는 무엇인가
pause shape, allocation rate, old generation growth 를 같이 보는 것입니다.
Q. heap dump 는 언제 떠야 하나
pause behavior 와 old generation growth 를 본 뒤 retained heap 이 계속 수상할 때가 좋습니다.
Read Next
- pause spike 가 allocation churn 보다 retained heap 쪽으로 보이면 Java Heap Dump 를 이어서 보세요.
- 같은 memory pressure 가 서비스 failure 로 가고 있다면 Java OutOfMemoryError 와 비교해 보세요.
- GC pressure 와 함께 CPU 도 오른다면 Java JVM CPU High 를 같이 보세요.
- 전체 Java 분기 지도를 다시 보려면 Java Troubleshooting Guide 로 돌아가면 됩니다.
Related Posts
Sources:
- https://docs.oracle.com/en/java/javase/21/troubleshoot/
- https://docs.oracle.com/en/java/javase/21/gctuning/
심사 대기 중에는 광고 대신 관련 가이드를 먼저 보여줍니다.
먼저 읽어볼 가이드
검색 유입이 많은 핵심 글부터 이어서 보세요.
- 미들웨어 트러블슈팅 가이드: 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를 푸는 실전 가이드입니다.
심사 대기 중에는 광고 대신 관련 가이드를 먼저 보여줍니다.