必须用 jmap -dump:format=b,file=路径.pid 导出内存快照,-histo 和 -finalizerinfo 无法分析对象引用链;导出后须用 MAT 查 Dominator Tree 和 Path to GC Roots 定位泄漏根因。

导出内存快照必须用 -dump,别碰 -histo 或 -finalizerinfo
很多人误以为 jmap -histo 能查大对象引用链,其实它只统计类实例数量和浅堆大小,完全不包含对象间的引用关系。真正能用于分析“谁持有了大对象”的,只有 jmap -dump 生成的 .hprof 文件。
正确命令是:
jmap -dump:format=b,file=/tmp/heap.hprof <pid></pid>
注意几个关键点:
-
format=b是必须的,表示二进制格式;省略会失败或输出无效内容 - 路径要有写权限,
/tmp最稳妥;不要写成./heap.hprof(可能因 JVM 工作目录不可写而静默失败) - JDK 8 及以前默认使用
Serial GC时,该命令会触发 Full GC;JDK 11+ 的ZGC/Shenandoah下仍可工作,但部分老版本 ZGC 不支持 —— 若卡住无响应,先jstat -gc确认 GC 类型 - 生产环境慎用:导出过程会暂停应用(STW),10GB 堆可能停顿 20 秒以上
jmap -dump 后必须用 MAT 或 JProfiler 打开,jhat 已淘汰且不支持引用链深度分析
jhat 虽然能启动 Web 页面查看堆,但它无法计算“支配树(Dominator Tree)”,也查不出某个 byte[] 是被哪个 HashMap、再被哪个 CacheService 持有 —— 这正是定位内存泄漏的关键路径。
推荐直接用 Eclipse MAT(Memory Analyzer Tool):
- 下载 standalone 版(无需 Eclipse IDE),解压即用
- 打开
heap.hprof后,点 “Dominator Tree” 视图,默认按“Retained Heap”倒序排列,顶部就是最可疑的大对象组 - 右键任意对象 → “Path to GC Roots” → 勾选 “with all references”,就能看到完整持有链(注意:若显示 “not all paths found”,说明对象已被回收或 dump 时已不可达)
- 如果对象是
char[]或byte[],MAT 会自动关联到其字符串内容(需开启 “Parse String Values” 选项)
分析时重点看 “Retained Heap” 而非 “Shallow Heap”,否则会漏掉间接引用膨胀
比如一个 ConcurrentHashMap 实例本身 Shallow Heap 只有 80 字节,但它存了 5 万个 ResponseDTO,每个 DTO 持有一个 1MB 的 byte[] —— 此时它的 Retained Heap 接近 50GB。只看 Shallow Heap 会完全错过这个根因。
MAT 中容易忽略的细节:
- “Group by Package” 或 “Group by Class Loader” 能快速识别第三方组件是否异常囤积对象(比如某 SDK 的静态缓存没清理)
- “Leak Suspects Report” 自动报告不一定准,尤其在多模块共用线程池场景下,得人工交叉验证 “Path to GC Roots” 和代码逻辑
- 如果发现大量
java.lang.ref.Finalizer占用 Retained Heap,不是 Finalizer 队列本身大,而是它背后挂了一堆待销毁资源 —— 此时要查finalize()方法里是否持有大缓冲区
线上不能跑 MAT?用 jcmd + jdk.jfr 替代 jmap 做轻量级采样
有些容器环境禁止 jmap(如 OpenShift 默认禁用 ptrace),或堆太大导出失败。这时可用 JDK 自带的飞行记录器做低开销采样:
jcmd <pid> VM.native_memory summary scale=MB<br>jcmd <pid> VM.native_memory detail scale=MB</pid></pid>
更进一步,启用堆分配采样(JDK 11+):
jcmd <pid> VM.native_memory summary scale=MB<br>jcmd <pid> VM.unlock_commercial_features<br>jcmd <pid> VM.native_memory summary scale=MB</pid></pid></pid>
不过真正替代 jmap -dump 的是:
jcmd <pid> VM.native_memory summary scale=MB<br>jcmd <pid> VM.unlock_commercial_features<br>jcmd <pid> VM.native_memory summary scale=MB</pid></pid></pid>
实际建议组合使用:
- 先用
jstat -gc看老年代增长趋势 - 再用
jcmd判断是否为 native 内存问题(如 Netty Direct Buffer 泄漏)VM.native_memory summary - 确认是 Java 堆后,才用
jmap -dump;若不可行,改用jcmd+VM.native_memory summary jstack交叉分析线程局部缓存行为
大对象占用链的根因往往藏在“看似合理”的缓存逻辑里,比如用了 WeakReference 却忘了清除 key,或者 ThreadLocal 在线程池中长期存活 —— 这些在 MAT 的 “Path to GC Roots” 里会暴露得非常直白,但前提是 dump 文件本身得真实反映那一刻的引用状态。










