JVM 内存问题排查方法论
0. 第一性原理:一个公式,三种病因
所有 JVM 内存问题(OOM / 频繁 FGC)本质都是一个失衡:
对象分配速率 > 内存回收速率
病因只有三类,排查就是判断是哪一类:
| 病因 | 含义 | 对应解法方向 |
|---|---|---|
| 该死不死 | 对象已无用但被引用,GC 无法回收 → 泄漏 | 找引用链 |
| 死得太慢 | 分区配置不合理,对象提前晋升老年代 | 调参数 |
| 生得太多 | 代码/流量导致分配速率过高 | 找分配热点 |
1. 四步流程:保全证据 → 定性 → 定位 → 验证
类比医生看病:先拍片留档,再判断病类,再精确病灶,最后复查。
第一步:保全证据(无论多急,最先做)
- dump 文件:事前靠
-XX:+HeapDumpOnOutOfMemoryError,事后靠jmap -dump - GC 日志:
-XX:+PrintGCDetails -Xloggc:... - 现场快照:
jstat -gcutil <pid> 1000、top
铁律:现场一旦重启丢失,靠猜的时间远大于分析 dump 的时间。先确认有 dump,再重启服务。
第二步:定性(不看代码,只看曲线)
用 jstat -gcutil <pid> 1000 连续采样,回答一个问题:
Full GC 之后,老年代(O 列)能不能降回去?
- 降不回去 → 泄漏类(进入引用链路径)
- 降得回去但 FGC 频繁 → 分配/晋升类(进入火焰图路径)
- 与 M 区/线程数相关 → 特殊类(Metaspace / 线程爆 / 堆外)
第三步:定位(按定性结果走三条路径)
路径 A · 泄漏类(粗筛 → 精查 → 定位):
jmap -histo:live <pid> | head -30 # 粗筛:哪个类实例/占用异常
jmap -dump → MAT 打开 # 精查:Leak Suspects / Dominator Tree
Path to GC Roots (排除 weak/soft) # 定位:引用链持有者 = 泄漏代码位置
路径 B · 分配类:
GC 日志丢 gceasy.io # 看:大对象分配?晋升失败?Young 太小?
Arthas profiler 火焰图 # 看哪个方法在疯狂分配对象
路径 C · 特殊类:
jstack <pid> # 线程爆(unable to create native thread)
-XX:+TraceClassLoading # Metaspace 类膨胀(反射/CGLIB)
-XX:MaxDirectMemorySize + Netty 排查 # Direct buffer
第四步:验证(没有验证的修复等于没修)
修复后观察三个指标是否回归平稳:
- 老年代水位曲线(锯齿状回落而非阶梯上升)
- FGC 间隔
- 分配速率
2. 决策树(可背下来的记忆锚点)
内存异常
├─ 已经 OOM ──→ 看报错关键词
│ heap space → dump + MAT 引用链
│ metaspace → 类加载排查
│ native thread → jstack 看线程数
│ direct buffer → 堆外/NIO
└─ 频繁 FGC ──→ Full GC 后 O 区回落吗?
降不回 → 泄漏 → histo → MAT → Path to GC Roots
降得回 → 分配太快 → GC日志/gceasy → 火焰图找热点
3. 工具三层观:望远镜、显微镜、行车记录仪
| 层 | 用途 | 工具 |
|---|---|---|
| 望远镜(定性看趋势) | 判断属于哪类病因 | jstat、GC 日志、gceasy、监控面板 |
| 显微镜(定位看细节) | 找到具体类/方法 | jmap histo、MAT、Arthas profiler、jstack |
| 行车记录仪(事前埋点) | 出事后有证据 | HeapDumpOnOutOfMemoryError、GC 日志参数 |
工具选择原则:能用望远镜定性,就不急着用显微镜;生产环境显微镜操作(dump、histo:live)都有 STW/触发 FGC 的代价,低峰执行。
4. 高频根因清单(经验先验,按概率排除)
- 静态 Map/List 当缓存,只 put 不 remove(无上限、无过期)
- 线程池 + ThreadLocal 不 remove
- 一次查全表的大 SQL 结果集进内存
- 大对象绕过 Young 区直接进老年代
- 依赖库显式
System.gc()(加-XX:+DisableExplicitGC) - 反射/动态代理导致类无限膨胀(Metaspace)
拿到问题先扫一遍这个清单,结合 jmap -histo 的类名,往往能直接命中。
5. 一句话总结
先保证据,再定性质(O 区降不降得回去),后用引用链或火焰图定位到代码,最后用曲线验证。