JVM GC 排查手册:从停顿日志到堆内存证据
遇到 Java 服务卡顿,直接修改堆大小或 GC 参数通常不是好起点。先回答三个问题:停顿是不是 GC 导致的、内存增长发生在 Java 堆还是堆外、增长的是正常业务工作集还是无法释放的对象。
先收集统一日志
现代 JDK 可以使用统一日志记录 GC 与 Safepoint:
-Xlog:gc*,safepoint:file=/logs/gc.log:time,uptime,level,tags:filecount=5,filesize=50m
重点关注:
- Young GC 的频率和每次回收效果。
- Old 区是否持续增长且回收后降不下来。
- 单次停顿是否与 P99 尖峰时间一致。
- 是否出现分配失败、晋升失败或 Full GC 连续发生。
- Safepoint 停顿是否来自 GC 以外操作。
不要只截取一行最长停顿。至少保留故障前后的完整窗口,并对照流量、发布、定时任务和下游异常。
在线诊断先用低风险命令
jcmd <pid> GC.heap_info
jcmd <pid> GC.class_histogram
jcmd <pid> Thread.print
jcmd <pid> JFR.start name=incident settings=profile duration=120s filename=/tmp/incident.jfr
Class Histogram 可以快速判断哪些类实例数量异常,但会带来一定停顿;JFR 更适合把分配、锁、线程和 I/O 放在同一时间线分析。Heap Dump 体积大且可能造成明显停顿,应确认磁盘空间,在副本或低峰执行:
jcmd <pid> GC.heap_dump /tmp/heap.hprof
区分堆内和堆外
容器 RSS 高于 -Xmx 并不异常,进程还包括 Metaspace、Code Cache、线程栈、Direct Buffer、JNI 和内存映射。启用 Native Memory Tracking 后可查看:
jcmd <pid> VM.native_memory summary
NMT 需要在启动时配置,会有额外开销。若线程数持续增长,单是线程栈就可能消耗大量内存;若 Netty Direct Memory 上升,则需要检查 Buffer 生命周期和池配置,不能只盯 Java Heap。
调优顺序
- 修复无界缓存、队列积压、监听器未注销等引用保留。
- 降低临时对象分配,避免大集合和大字符串反复复制。
- 给堆设置与容器限制匹配的上限,给堆外和系统留空间。
- 基于延迟目标选择收集器,而不是照抄别人的启动参数。
- 每次只改一组参数,在相同流量下做长时间对比。
对于延迟敏感服务,平均停顿没有 P99/P999 有意义;对于吞吐任务,过度追求低停顿可能提高 CPU 成本。堆越大也不是越好,更大的存活集可能带来更长扫描时间和更慢的故障恢复。
故障现场清单
- 保存 GC 日志、JFR、线程栈、容器指标与请求指标。
- 记录 JDK 版本、GC 类型、堆配置和容器限制。
- 检查 OOM 类型,是 Java heap、Metaspace、Direct buffer 还是系统 OOM Kill。
- 保留发布版本和流量变化时间线。
- 修复后用同样场景回放,验证停顿与内存都改善。

评论
0 条讨论