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 쪽으로 보이면 힙 덤프를 추출해 객체 수명과 참조 트리를 분석해 보세요.
- 같은 memory pressure 가 서비스 failure 로 가고 있다면 OOM 발생 지점의 메모리 상태부터 파악해 보세요.
- GC pressure 와 함께 CPU 도 오른다면 스레드 덤프를 통해 실제 과열된 스레드(Hot Thread)가 GC 스레드인지 확인해 보세요.
- 전체 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, RabbitMQ, Kafka 중 어디부터 볼까 Redis, RabbitMQ, Kafka가 함께 있는 시스템에서 지금 보이는 장애가 어느 계층에 더 가까운지, 첫 10분 안에 무엇을 확인하고 어떤 글로 들어가야 하는지 정리한 실전 허브 가이드입니다.
- Kubernetes CrashLoopBackOff: 먼저 볼 것들 startup failure, probe, config, resource limit 관점에서 CrashLoopBackOff를 어떻게 나눠서 봐야 하는지 정리한 가이드입니다.
- Astro 기술 블로그 SEO 체크리스트: 트래픽 기다리기 전에 먼저 고칠 것 Astro 기술 블로그를 위한 실전 SEO 체크리스트입니다. 배포 호스트 확인, robots.txt, sitemap, canonical, hreflang, 구조화 데이터, 페이지별 메타데이터, noindex 판단, 검증 명령까지 우선순위대로 정리합니다.
- 다국어 블로그 canonical과 hreflang 설정 가이드: 무엇을 확인하고 어디서 깨질까 다국어 블로그에서 canonical과 hreflang을 어떻게 설정해야 하는지 실전 기준으로 정리합니다. self-canonical, 상호 연결되는 hreflang 묶음, x-default, 카테고리 페이지, 최종 렌더 HTML 점검, 한 언어 버전이 다른 언어 버전을 눌러버리는 실수까지 다룹니다.
- OpenAI Codex CLI 설치 가이드: 설치, 인증, 첫 작업까지 OpenAI Codex CLI를 실전 기준으로 설치하는 방법을 정리했다. 설치, 로그인, 첫 실행, Windows 주의점, 첫 작업을 어떻게 시작하면 좋은지까지 다룬다.