内存泄漏典型表现为老年代持续增长且Full GC无法回收、堆使用率居高不下、RSS远超-Xmx、响应变慢甚至OOM;定位需三步:抓堆快照、MAT分析支配树、回溯GC Roots引用链。

明确内存泄漏的典型表现
Java 应用运行一段时间后,老年代(Old Gen)持续增长,即使触发多次 Full GC 也回收不了多少对象;堆内存使用率居高不下,JVM 进程 RSS 内存远超 -Xmx 设置值;应用响应变慢、频繁卡顿,甚至抛出 java.lang.OutOfMemoryError: Java heap space。这些不是单次 GC 暂停长的问题,而是对象长期无法被回收的累积效应。
快速定位泄漏源头的三步法
不依赖猜测,靠数据链路闭环验证:
-
抓堆快照(Heap Dump):在内存明显升高时,用
jmap -dump:format=b,file=heap.hprof <pid>获取实时堆镜像;生产环境建议加-XX:+HeapDumpBeforeFullGC自动触发 - 用 MAT 分析支配树(Dominator Tree):打开 hprof 文件后,优先看 “Leak Suspects” 报告;再深入 Dominator Tree,找 Retained Heap 最大、且不应长期存活的对象(如某个 Map、缓存容器、监听器列表)
- 回溯 GC Roots 引用链:右键可疑对象 → “Path to GC Roots” → 选 “with all references”,重点看哪条路径意外持有了它(例如静态集合未清理、ThreadLocal 未 remove、内部类隐式持有外部类实例)
高频泄漏场景与规避写法
80% 的泄漏来自几类惯性错误:
-
静态集合无清理:如
private static final Map<String, Object> cache = new HashMap<>();—— 改用WeakHashMap或引入定时清理+容量限制 -
ThreadLocal 泄漏:线程池复用导致 ThreadLocal 变量跨请求残留 —— 每次使用后显式调用
tl.remove(),尤其在 Filter/Interceptor 中 - 监听器/回调未注销:注册了 Observer、Listener、Spring ApplicationListener 却忘记反注册 —— 确保在生命周期结束处(如 destroy()、afterPropertiesSet())配对移除
-
内部类持外部引用:非静态内部类默认强引用外部实例,若被长期持有(如传给线程、缓存),会导致整个外部对象无法回收 —— 改为静态内部类 + 显式弱引用(
WeakReference<Outer>)
日常防御性实践建议
把排查成本前置到开发和上线环节:
立即学习“Java免费学习笔记(深入)”;
- 本地启动时加
-XX:+PrintGCDetails -XX:+PrintGCTimeStamps,观察 GC 频率与老年代趋势 - 关键模块上线前做轻量级压测 + 堆快照对比(初始 vs 1小时后),用 MAT 的 “Compare Basket” 功能查新增大对象
- 在 CI 流程中集成
jdk.jfr录制(-XX:StartFlightRecording=duration=60s,filename=recording.jfr),结合 JMC 查内存分配热点 - 代码审查重点关注
static、ThreadLocal、new Thread、匿名内部类等关键词


















