JVM 内存问题排查方法论

JVM 内存问题排查方法论

0. 第一性原理:一个公式,三种病因

所有 JVM 内存问题(OOM / 频繁 FGC)本质都是一个失衡:

复制代码
对象分配速率 > 内存回收速率

病因只有三类,排查就是判断是哪一类:

病因 含义 对应解法方向
该死不死 对象已无用但被引用,GC 无法回收 → 泄漏 找引用链
死得太慢 分区配置不合理,对象提前晋升老年代 调参数
生得太多 代码/流量导致分配速率过高 找分配热点

1. 四步流程:保全证据 → 定性 → 定位 → 验证

类比医生看病:先拍片留档,再判断病类,再精确病灶,最后复查。

第一步:保全证据(无论多急,最先做)

  • dump 文件:事前靠 -XX:+HeapDumpOnOutOfMemoryError,事后靠 jmap -dump
  • GC 日志:-XX:+PrintGCDetails -Xloggc:...
  • 现场快照:jstat -gcutil <pid> 1000top

铁律:现场一旦重启丢失,靠猜的时间远大于分析 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 区降不降得回去),后用引用链或火焰图定位到代码,最后用曲线验证。

相关推荐
wuminyu2 小时前
Kafka利用sendfile与Page Cache实现高性能传输剖析
java·linux·c语言·jvm·c++
my_realmy3 小时前
Java 基础面试笔记(二):JDK、JRE、JVM、JIT 与 JAR
jvm·虚拟机·jit·特性·jre·垃圾回收机制
Nuanyt3 小时前
JUC常见核心知识梳理01 线程 并发 JMM volatile 管程 锁 synchronized
java·开发语言·网络·jvm
CodeStats3 小时前
【Java 类加载器】Java 类加载器完整体系深度拆解(中):URLClassLoader 能力剖析与继承委派辨析
java·jvm·classloader·类加载器
驭渊的小故事13 小时前
多线程02
java·开发语言·jvm
CodeStats20 小时前
【Java 类加载器】Java 类加载器完整体系深度拆解(上):从 JVM 启动到双亲委派模型
java·开发语言·jvm·classloader·双亲委派
CodeStats21 小时前
【Java类加载器】Java 类加载器完整体系深度拆解(下):自定义加载器实战与 SPI 破坏双亲委派(MySQL 驱动揭秘)
java·开发语言·jvm·classloader·双亲委派
Nuanyt1 天前
JVM常见核心知识梳理02 类加载 字节码技术 双亲委派 Java内存模型 JMM 并发底层 volatile synchronized 常见排障与调优工具
java·开发语言·jvm
焦虑的说说1 天前
jvm知识汇总
jvm