定位内存泄漏需先确认老年代持续增长且Full GC回收极少,再通过jstat监控、堆转储分析(MAT/VisualVM)找出高频未释放对象,重点排查静态集合、未注销监听器、非静态内部类及ThreadLocal等强引用持有源,最后用相同操作复现并对比jstat与堆快照验证修复效果。

定位频繁分配但无法回收的对象,关键在于识别“活”着却不该存在的对象——它们被意外持有引用,逃过了垃圾回收。这类对象往往数量多、生命周期异常长、占用堆空间持续增长。
观察内存增长趋势,确认泄漏存在
先判断是不是真问题:用 JVM 自带工具监控老年代使用率和 Full GC 频次。如果老年代内存持续上升、Full GC 后回收量极少,甚至触发 java.lang.OutOfMemoryError: Java heap space,基本可断定有对象在堆积。
- 运行时实时查看:
jstat -gc <pid> 2000(每2秒刷新一次GC统计) - 自动捕获快照:启动时加参数
-XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/path/to/dumps/ - 手动触发快照:
jmap -dump:format=b,file=heap.hprof <pid>
聚焦高频创建又未释放的类实例
在堆转储文件(如 heap.hprof)中,用 Eclipse MAT 或 VisualVM 打开后,重点看“Classes”视图或“Dominator Tree”:
- 按“Objects”列排序,找出实例数异常高的类(比如几万甚至上百万个 String、HashMap.Entry、自定义 DTO)
- 右键 → “Merge Shortest Paths to GC Roots”,排除 classloader、system class 等合法根引用
- 关注带有“exclude weak/soft references”选项的路径——真正阻止回收的,往往是强引用链
检查典型持有源,快速缩小范围
90% 的高频对象泄漏,集中在以下几类持有关系中:
- 静态集合缓存:static Map/List 持有业务对象,且无清理逻辑(如未配过期、未按需 remove)
- 监听器/回调未注销:注册了 OnClickListener、BroadcastReceiver、Netty ChannelHandler 等,但 Activity 销毁或连接关闭后没反注册
- 非静态内部类隐式引用:Handler、Runnable、AsyncTask 内部类持外部 Activity/Fragment 引用,被线程池长期持有
- ThreadLocal 未清理:在线程复用场景(如 Tomcat 线程池)中 set 后未 remove,导致 value 对象随线程常驻
验证与修复后闭环确认
改完代码不能只靠“感觉”,要实证是否解决:
- 用相同操作路径复现(如反复打开关闭页面、循环调用某接口)
- 对比修复前后 jstat 输出:老年代增长速率明显下降,Full GC 次数减少
- 再次 dump 堆并比对:目标类实例数不再累积,GC Roots 引用链消失

















