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 区降不降得回去),后用引用链或火焰图定位到代码,最后用曲线验证。

相关推荐
晚安code2 小时前
JVM 内存结构入门:程序计数器、虚拟机栈、本地方法栈与堆的溢出诊断
java·jvm
老三牛擦3 小时前
多平台上架
jvm
自强的小白5 小时前
jvm面试(Gc)
服务器·jvm·面试
晚安code6 小时前
JVM 垃圾回收全流程拆解:可达性分析、四种算法与五种收集器选型
jvm
用户094248568036 小时前
第20章:JVM G1 GC原理、日志与调优实战
java·jvm
艾莉丝努力练剑8 小时前
【AI大模型接入SDK】ChatSDK 集成测试概述
jvm·c++·人工智能·学习·面试·集成测试·sdk
Joe_Wang58 小时前
【从0到1学习JVM · 14】堆内存各区域分工与Xms和Xmx设为一样的真相
java·jvm·学习·垃圾回收
HwJack208 小时前
【HarmonyOS开发小实践】HarmonyOS GC 引用计数 vs 对象追踪,三种回收算法
jvm·算法·harmonyos
_upupup1 天前
异常(C++)
java·开发语言·jvm
老三牛擦1 天前
掌握数据结构及算法、计算机组成原理、操作系统、计算机网络和软件工程基础知识,践行Scrum敏捷开发理念
jvm