Java内存回收不及时主因是强引用导致对象滞留,需通过jstat监控老年代增长、jmap+MAT分析GC Roots路径,重点排查静态缓存、未注销监听器、非静态内部类隐式引用及ThreadLocal泄漏,并验证修复后实例数与引用路径是否消除。

Java 内存回收不及时,往往不是 GC 失效,而是对象被强引用链牢牢“钉”在堆里——它还“活着”,只是不该活那么久。排查关键不在等 GC,而在揪出那些本该释放却仍被强引用持有的对象。
看老年代是否真在持续堆积
强引用残留的典型表现是老年代内存缓慢但稳定上涨,Full GC 后回收量极少(比如只回收几 MB,而老年代已占 80%+)。这时别急着分析代码,先确认问题存在:
- 用 jstat -gc
2000 每两秒观察:重点关注 OU(Old Used) 是否单向增长、FGC(Full GC 次数) 是否频繁但 FGCT(Full GC 耗时) 后 OU 下降不明显 - 出现 java.lang.OutOfMemoryError: Java heap space 且 dump 文件中老年代占满,基本可锁定强引用滞留
从堆转储里找“数量异常多”的类实例
用 jmap -dump:format=b,file=heap.hprof
- 进入 Classes 视图,按 Objects 列降序排列,重点盯住实例数达数万、数十万的类(如
HashMap$Node、String、自定义OrderDTO等) - 右键目标类 → Merge Shortest Paths to GC Roots,勾选 exclude weak/soft references —— 这一步只保留真正阻止回收的强引用路径
- 若路径终点是
java.lang.Thread、static field或java.util.HashMap等容器,就极可能是强引用残留源头
重点检查四类高频强引用持有场景
90% 的强引用残留集中在这几处,按优先级逐个核对:
立即学习“Java免费学习笔记(深入)”;
-
静态集合缓存:如
private static Map<string user> userCache = new HashMap();</string>,只 put 不 remove,也不设过期策略 -
监听器/回调未注销:Activity 注册了 BroadcastReceiver、View 设置了 OnClickListener,但在 onDestroy() 中忘了
unregister或setOnClickListener(null) - 非静态内部类隐式持外部引用:Handler、Runnable、AsyncTask 声明在 Activity 内部且被线程池或延时任务长期持有,导致整个 Activity 实例无法释放
-
ThreadLocal value 泄漏:在 Tomcat 等线程复用环境中,set 了一个大对象进 ThreadLocal,但没在 finally 块中调用
remove()
验证修复是否真正切断强引用链
改完代码不能只信逻辑,要拿数据说话:
- 用相同操作路径反复触发(如打开/关闭同一页面 10 次),再执行一次 jstat 对比 OU 增速是否显著放缓
- 重新 dump 堆,对比修复前后目标类的实例数和 GC Roots 路径 —— 正确修复后,路径应消失或指向明确的、生命周期合理的引用点(如当前活跃的 Activity 实例)
- 若仍存在强引用路径,回到 MAT 中展开路径逐层查看,特别注意中间是否有 static 字段、未清理的容器或未结束的线程


















