JVM 内存问题排查方法论

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

第四步:验证(没有验证的修复等于没修)

修复后观察三个指标是否回归平稳:

  1. 老年代水位曲线(锯齿状回落而非阶梯上升)
  2. FGC 间隔
  3. 分配速率

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. 高频根因清单(经验先验,按概率排除)

  1. 静态 Map/List 当缓存,只 put 不 remove(无上限、无过期)
  2. 线程池 + ThreadLocal 不 remove
  3. 一次查全表的大 SQL 结果集进内存
  4. 大对象绕过 Young 区直接进老年代
  5. 依赖库显式 System.gc()(加 -XX:+DisableExplicitGC)
  6. 反射/动态代理导致类无限膨胀(Metaspace)

拿到问题先扫一遍这个清单,结合 jmap -histo 的类名,往往能直接命中。


5. 一句话总结

先保证据,再定性质(O 区降不降得回去),后用引用链或火焰图定位到代码,最后用曲线验证。

相关推荐
寂寞的船13 小时前
【Unity知识速递】第10期 Unity GC 零分配实战:从 UnityEngine.Pool 到手动 GC 控制的全链路优化
jvm·unity·c#·游戏引擎·lucene
金銀銅鐵2 天前
同一个 Hello World,Kotlin 和 Java 在字节码层面差在哪
jvm·后端·kotlin
yeccheng3 天前
MySQL 索引超详解:从底层 B+ 树到面试高频考点
java·开发语言·jvm·mysql·面试
On The Road :6 天前
深入理解 JVM 垃圾回收:GC 是什么、如何判定垃圾、四大回收算法全解析
jvm·算法
Yves..6 天前
JVM 学习笔记:内存区域、类加载机制与垃圾回收
jvm
akinggw6 天前
libagentcore - 一个基于c++的AI多agent系统升级到2.0版本
开发语言·jvm·c++
yh弓长7 天前
JVM 核心机制
jvm
tg_xianheyun8 天前
腾讯云国际账号注册代充值适合哪些海外业务场景?
jvm
yuniko-n8 天前
【JVM】对象内存布局
java·开发语言·jvm
wuminyu8 天前
JVM执行引擎学习总结:热点字节码编译成本地机器码
java·linux·c语言·jvm·c++