确认内存泄漏需观察老年代使用率“只涨不跌”、fgc频次激增且回收无效、oom频繁复现、特定类实例持续增长;jmap加live可导出纯净堆快照;mat中通过path to gc roots查强引用链,dominator tree定位真正内存支配者。

怎么确认真有内存泄漏,而不是GC波动?
别急着 jmap,先看现象是否符合泄漏本质:老年代内存「只涨不跌」。比如 jstat -gcutil <pid> 1000 5</pid> 连续采样,发现 O(Old space 使用率)从 70% → 85% → 92% → 96% → 98%,且每次 FGC 后几乎没回落,这就不是正常业务高峰,是泄漏信号。
- Full GC 频次越来越高(比如从每小时 1 次变成每分钟 2 次),单次耗时超 1s,同时
FGCT累计值快速上升 - 应用日志里反复出现
java.lang.OutOfMemoryError: Java heap space,重启后几小时内复现 - 用
jmap -histo:live <pid></pid>隔 5 分钟跑两次,对比发现某类实例数(如com.example.UserCacheEntry)持续增长、不减
用 jmap 导出 dump 时,为什么加 live 很关键?
jmap -dump:format=b,file=heap.hprof <pid></pid> 会触发一次全局 Stop-The-World,导出所有对象(包括已标记待回收但还没清理的)。而生产环境更推荐 jmap -dump:live,format=b,file=heap.hprof <pid></pid> —— 它强制先做一次 Full GC,再 dump 剩下的「活对象」,数据更干净,避免把临时对象噪音带进分析。
- 不加
live:dump 文件大、MAT 分析慢、容易被短生命周期对象干扰判断 - 加
live:文件小 30%~50%,Leak Suspects报告更准,但会短暂卡顿(STW 时间取决于老年代大小) - 线上慎用:若服务敏感,优先依赖启动参数
-XX:+HeapDumpOnOutOfMemoryError自动捕获,而非手动触发
MAT 中怎么看懂「Path to GC Roots」里的引用链?
打开 dump 后点 Leak Suspects Report,它标出的嫌疑对象只是起点;真正要动手改代码,得点进去看 Path to GC Roots —— 这里显示的是「谁在强持有这个对象,让它没法被回收」。
- 看到
static字段(如com.example.CacheManager.cache)→ 典型静态集合泄漏,检查是否漏清理或没设过期 - 看到
java.lang.Thread持有 → 查线程栈,可能是线程池任务中缓存了大对象或未关闭ThreadLocal - 看到
org.springframework.context.support.AbstractApplicationContext→ Spring Bean 生命周期异常,比如监听器注册后没注销 - 注意过滤:勾选
exclude all phantom/weak/soft references,只看强引用链,否则会看到大量无意义路径
为什么用 Dominator Tree 比 Histogram 更快定位根因?
Histogram 只告诉你「哪个类实例最多」,比如 byte[] 占内存第一——但这没用,因为几乎所有大对象底层都是 byte[]。Dominator Tree 才是关键:它按「支配关系」排序,排第一的,是那些「自己占内存多 + 它活着就拖着一堆其他对象也活不了」的对象。
立即学习“Java免费学习笔记(深入)”;
- 找
Retained Heap最大的几行,比如com.example.OrderService实例占 1.2GB → 直接去查这个类的静态字段或单例状态 - 右键 →
Path to GC Roots→ 看是不是被某个static Map或未 shutdown 的ScheduledExecutorService持有 - 如果
Dominator Tree顶部全是第三方库类(如io.netty.buffer.PooledByteBuf),别急着改自己代码,先查 Netty 是否漏调release()
真正难的不是找到那个大对象,而是看懂它为什么不该活那么久——引用链里每个箭头,都对应一行可能漏掉的 remove()、close() 或 shutdown()。








