关键在于建立“观察→猜测→验证→定位”闭环思维:先用jstat/jconsole确认老年代单向爬升,再用jmap+MAT找泄漏对象,通过Path to GC Roots识别static/监听器/ThreadLocal等强引用,最后靠代码规范预防。

从零开始学 JVM 内存泄漏排查,关键不是堆工具命令,而是建立“观察 → 猜测 → 验证 → 定位”的闭环思维。掌握顺序错了,容易陷入记命令、看图形却看不懂问题本质的困境。
先学会看内存是不是真在漏
别急着生成堆转储。第一步是确认现象是否符合内存泄漏特征:
- 用 jstat -gcutil <pid> 2000 10 每两秒查一次,重点盯 OU(老年代使用率)是否单向爬升,且每次 Full GC 后不回落或回落极少
- 配合 jconsole 连上进程,观察“Memory”页签中“Old Gen”曲线——平缓上升+无明显回收谷底,比日志更直观
- 注意区分假信号:比如刚启动时老年代缓慢上涨属正常;而运行数小时后每分钟涨 2MB,持续 30 分钟不降,才值得深入
快速生成并读懂第一份堆快照
确认可疑后,立刻抓现场数据,别等 OOM:
- 执行 jmap -dump:format=b,file=heap.hprof <pid>(建议加
live参数:-dump:live,format=b,file=...,跳过待回收对象,分析更干净) - 用 Eclipse MAT 打开,直接点 “Leak Suspects Report” —— 它会自动标出最可能泄漏的 1–3 个对象组,比如 “One instance of ‘java.util.ArrayList’ dominates heap”
- 点进报告里的 “Details”,看 “Accumulated Objects by Class” 表,找实例数异常多、总大小排前三的类(例如 HashMap$Node 占 400MB,但业务逻辑里不该存这么多)
顺着引用链找到“卡住”的那个根
MAT 报告只是起点。真正定位靠的是看谁“拽着不放”:
你接手一个需求时,是不是经常这样:"改一个小字段 → 结果把不相关的代码也改了"、"需求说得很模糊 → 做着做着发现跑偏了"、"数据库加个字段 → 上线才发现漏了数据兼容"、"做了个新功能 → 一个月后自己都忘了当时怎么设计的"、"写到一半被中断 → 再回来不知道从哪继续";这个 Skill 就是专门解决这些问题的。 它会自动探测你的项目环境(JDK、Spring Boot、ORM、数据库、前端框架),记在
立即学习“Java免费学习笔记(深入)”;
- 右键可疑对象 → “Path to GC Roots” → 勾选 “with all references” → 查看完整引用链
- 重点识别三类“不该存在的强引用”:static 字段(如
private static Map<String, Object> cache = new HashMap<>();)、未注销的监听器(如 addListener 后没 remove)、线程局部变量(ThreadLocal)持有大对象且线程长期存活 - 如果引用链终点是
java.lang.Thread或java.util.concurrent.ThreadPoolExecutor,大概率是线程池任务没结束,或 ThreadLocal 泄漏
用代码和配置把漏洞堵在上线前
排查是救火,预防才是核心能力:
- 静态集合必须配清理机制:用 WeakHashMap 替代 HashMap 存缓存;或定时任务调用
map.clear()+ 注册 JVM shutdown hook 做兜底 - 所有注册监听/回调的地方,强制配套注销逻辑(IDE 可配置 live template:输入
reg自动补全 register + unregister 模板) - JVM 启动加参数:
-XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/opt/dumps/,让 OOM 自动留证;再加-XX:+PrintGCDetails记录 GC 日志,方便回溯
不复杂但容易忽略:多数泄漏不是技术难点,而是开发时少写了一行 unregister、忘了清空 static List、或测试没跑够时间。把检查项变成日常习惯,比精通 MAT 更管用。

















