Java内存泄漏排查需按“谁申请、谁未释放、为何未释放”三步还原证据链:先分堆内/堆外/Metaspace类型并用对应工具定位;再用MAT或NativeMemoryTracking分析强引用链或native分配;最后验证生命周期匹配性与释放逻辑健壮性。

明确泄漏类型:先分区域,再定工具
堆内泄漏和堆外泄漏的排查路径完全不同,第一步必须锁死问题区域:
-
堆内泄漏:表现是
java.lang.OutOfMemoryError: Java heap space,老年代持续增长、GC后存活对象不降,重启后恢复但几小时又复现;用jstat -gcutil看 YGC/FULL GC 频率与回收量是否失衡,确认后立即用jmap -dump:format=b,file=heap.hprof <pid>抓堆转储。 -
堆外泄漏:典型症状是 RSS 远超
-Xmx(比如-Xmx2g但 RSS 到 9GB),堆监控一切正常,Direct buffer memoryOOM 或容器被 cgroup OOMKilled;此时重点查NativeMemoryTracking(启动加-XX:NativeMemoryTracking=detail)或cat /proc/<pid>/smaps | grep -i "direct\|anon\|heap"定位内存大户。 -
Metaspace 泄漏:错误提示为
OutOfMemoryError: Metaspace,常见于频繁动态生成类(如 CGLIB 代理、JSON 反序列化大量新类型),用jstat -gc <pid>观察 MCMN/MC/CCSC 等列变化趋势。
堆内泄漏:MAT 分析要盯住“活对象”而非总数
MAT 不是用来数对象个数的,而是找“不该活却一直活着”的对象。关键操作有三步:
- 打开 dump 后先跑 Leak Suspects,它自动识别出占用最大、且有可疑引用链的对象集合(比如某个静态 Map 占用 78% 堆);
- 点进该集合,右键 → Path to GC Roots → exclude all phantom/weak/soft references,只保留强引用链,看哪条路径把本该回收的对象“钉”在堆里;
- 对照业务代码,重点检查:静态缓存未设过期、监听器注册后未反注册、ThreadLocal 没清理、流/连接未 close、异步任务提交后丢失引用等高频泄漏点。
堆外泄漏:从 DirectByteBuffer 顺藤摸到 native 层
Netty、NIO 等框架大量使用 DirectByteBuffer,它本身小(堆内几十字节),但背后可能挂着几百 MB 的 native 内存:
- 用
jcmd <pid> VM.native_memory summary查 direct memory 当前用量; - 开启 JVM 参数
-Dio.netty.leakDetectionLevel=paranoid,让 Netty 在 ByteBuf 未释放时打完整调用栈; - 若仍无法定位,用
jdk.jfr录制内存分配事件:jcmd <pid> VM.start_flightrecording name=leak duration=60s settings=profile,导出后分析jdk.NativeMemoryUsage事件,结合线程栈锁定分配源头。
验证与闭环:不能只看“有没有”,还要看“为什么没释放”
找到嫌疑对象只是开始,真正闭环要回答两个问题:
立即学习“Java免费学习笔记(深入)”;
- 这个对象的生命周期是否与业务语义匹配?比如一个订单处理完,关联的上下文对象是否还被某个全局队列持有?
- 释放逻辑是否存在条件竞争或异常绕过?比如 try-with-resources 被手动 close 替代,而 catch 块里忘了 close;或者回调中抛异常导致 finally 未执行。
- 上线修复后,必须配置
-XX:+HeapDumpOnOutOfMemoryError和-XX:HeapDumpPath,并用 Prometheus + Grafana 监控老年代使用率 7 天趋势,确认不再爬升。


















