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

相关推荐
Irene199114 小时前
Flink 学习日志 01:前端转大数据的实战避坑指南
java·jvm·flink
小天源1 天前
从 Nginx 日志到访问分析:轻量化网站监测服务的架构实践
jvm·nginx·网络安全·架构·网络监控
十年Java程序媛1 天前
JVM 内存区域与对象内存布局|new 对象底层流程,对象头 MarkWord 详解
java·jvm·spring boot
用户094248568031 天前
第2章:Windows、WSL2 与 Docker 环境搭建Redis
java·jvm
梅梅绵绵冰1 天前
JVM虚拟机
java·开发语言·jvm
十年Java程序媛2 天前
JVM 基础与 GC 垃圾回收详解|可达性分析、分代模型、垃圾收集器、STW
java·开发语言·jvm·spring boot·后端·面试
Lhappy嘻嘻2 天前
读懂 JVM:内存模型、类加载机制、分代回收
java·开发语言·jvm
CS创新实验室3 天前
Python 对象“死“了之后去了哪?——引用计数与分代垃圾回收
开发语言·jvm·python
wuminyu3 天前
JMM学习总结:深入理解happens-before关系
java·linux·c语言·jvm·c++
DongQiShanRen3 天前
六根硬件映射(下):物理闭环五步法,抽一条已实装的看
大数据·jvm·人工智能·算法·rust·哈希算法·集成学习