掌握jvm内存泄漏分析关键在于闭环思维:从问题出发,通过现象定位证据,再追溯gc root根因;需区分堆内/堆外泄漏,善用对比分析与工具链协同排查。

掌握 JVM 内存泄漏高级分析技巧,关键不在工具堆砌,而在建立“问题—现象—证据—根因”的闭环思维。真正卡住人的,往往不是不会点 MAT 的饼图,而是看到百万个 HashMap$Node 却不知道该顺着哪条引用链往下挖。
搞懂 GC Root 的真实构成,别只背教科书定义
GC Root 不是静态列表,而是运行时动态快照中的“存活锚点”。比如:
- 一个被
ThreadLocal引用的对象,Root 是线程栈里的局部变量 + 当前线程对象本身; - Netty 的
PooledByteBuf如果泄漏,Root 很可能落在PoolThreadCache的线程本地缓存中,而非你写的业务类; - Spring 的单例 Bean 被静态持有,Root 就是
ApplicationContext对应的 ClassLoader + 静态字段引用链。
建议每次导出堆转储后,先在 MAT 中打开 “Leak Suspects Report”,再手动点开 “Dominator Tree” → 右键任一可疑大对象 → “Path to GC Roots” → 勾选 “with all references”,观察完整路径里哪些环节是“本不该存在却一直挂着”的引用。
区分堆内泄漏和堆外泄漏,别让直接内存背锅或漏查
很多 OOM 表现为进程被 kill -9、RSS 持续上涨、但堆内存(jstat -gc)平稳——这大概率是直接内存泄漏。常见来源:
-
java.nio.DirectByteBuffer:尤其在 Netty、gRPC、NIO 文件读写场景中,未调用cleaner.clean()或未显式buffer.clear()/buffer.free(); - JNI 调用:如图像处理库、加密 SDK 使用 native malloc 分配后未释放;
- 元空间(Metaspace)膨胀:动态生成类(Groovy/ASM/CGLIB)、大量热部署、未设置
-XX:MaxMetaspaceSize。
验证方法:pmap -x <pid></pid> 查 RSS 总量,cat /proc/<pid>/smaps | grep -i "direct\|heap\|meta"</pid> 定位大块内存归属;JDK 17+ 可用 jcmd <pid> VM.native_memory summary</pid> 对比 baseline。
用好“对比分析法”,单次快照容易误判
一次堆转储只能告诉你“现在有什么”,不能说明“为什么涨”。高级分析必须做差值:
- 在业务低峰期(如凌晨2点)dump 一份 baseline;
- 在内存明显上升后(如连续 Full GC 后)再 dump 一份;
- 用 MAT 的 “Compare Basket” 功能,把两份 heap dump 加载进来,按 class 名筛选,看哪些类实例数/总大小增幅最大;
- 重点关注:新增的
char[]、byte[]、ConcurrentHashMap$Node、LinkedBlockingQueue$Node—— 它们往往是泄漏容器的底层存储。
把工具链串起来,形成可复用的排查流水线
线上不能只靠 MAT 离线分析。要构建“监控→捕获→定位→验证”四步链路:
-
监控层:用 Prometheus + JVM Exporter 抓取
jvm_memory_used_bytes(按 memory pool 维度)、jvm_gc_collection_seconds_count,配置“老年代使用率 24h 上升超 30%”告警; -
捕获层:通过 Arthas
vmtool --action getInstances --className java.util.HashMap --limit 10快速采样,或用jmap -dump:format=b,file=/tmp/heap.hprof <pid></pid>触发快照(注意磁盘空间); -
定位层:MAT 中用 OQL 查询:
SELECT * FROM java.util.HashMap WHERE (objects.size > 10000),快速筛出异常大 Map; -
验证层:修复后上线,用
jstat -gc <pid> 5000</pid>观察老年代是否稳定、Full GC 是否消失,而非只看内存曲线回落。
不复杂但容易忽略











