核心是确认“本该被回收的对象为何还活着”,关键不在类定义本身,而在对象是否被意外、长期持有;需先用jstat验证老年代持续上涨且full gc回落极少,再通过jmap -histo定位增长对象,最后用mat分析gc roots路径锁定static字段、单例、threadlocal等强引用源头。

排查 Java 中类与对象引发的内存泄漏,核心是确认“本该被回收的对象为何还活着”——关键不在类定义本身,而在对象是否被意外、长期持有。重点不是看类写了什么,而是看哪些实例被谁引用、为何不释放。
先验证:是不是真泄漏?
别一上来就翻代码,先用数据说话:
- 用 jstat -gc
1000 持续观察:老年代(Old)使用率持续上涨,Full GC 后回落极少,且频率越来越高 - 监控堆内存曲线:长时间运行后呈单调上升趋势,尤其在业务低峰期也不回落
- 出现 java.lang.OutOfMemoryError: Java heap space,且重启后几小时内复现
定位可疑类和对象
从宏观到微观缩小范围:
在 Java 中初始化和管理阿里云 SDK客户端。包括单例模式、线程安全、endpoint 与 region 配置、VPC 终端节点、同步与异步等。
- 执行 jmap -histo:live
,重点关注实例数多、总大小高的类(如 byte[]、HashMap、自定义实体类、DTO 等) - 对比多次执行结果:哪些类的实例数/总大小在稳定增长?记下类名(如 com.example.UserCache、org.springframework.web.context.ContextLoader)
- 生成堆快照:jmap -dump:format=b,file=heap.hprof
,用 VisualVM 或 Eclipse MAT 打开
分析引用链:谁在“钉住”这些对象?
打开 heap.hprof 后,聚焦 GC Roots 路径:
- 在 MAT 中点 “Leak Suspects Report”,它会自动标出最可能泄漏的几个对象组
- 对目标大对象右键 → “Path to GC Roots” → 选 “exclude weak/soft references”(排除弱软引用,聚焦强引用)
- 重点看路径终点:是不是 static 字段(如 com.example.ConfigManager.INSTANCE)、Spring 单例 Bean、线程局部变量(ThreadLocal)、监听器列表、或未关闭的资源包装类?
- 常见“钉子”位置:static 集合、单例中缓存字段、注册未注销的 Listener、内部类隐式持外部类、ThreadLocal 未 remove()
查代码:围绕引用链终点深挖
拿到 GC Roots 路径终点类后,针对性检查其字段和逻辑:
- 如果是 static 字段:查它是否直接/间接持有业务对象(如 private static Map
cache ),是否设了上限、过期、清理入口? - 如果是 Spring 单例 Bean:查它是否用 @PostConstruct 初始化了静态集合,或在方法中往全局容器里 put 了对象却没配 @PreDestroy 清理
- 如果是 ThreadLocal:查 set() 后是否在 finally 块里调用了 remove(),尤其在异步或连接池场景下
- 如果是监听器/回调:查注册处(addXXXListener)是否有对应注销逻辑(removeXXXListener),是否在对象销毁时漏掉了
修复与验证
改完代码后必须闭环验证:
- 加显式清理逻辑(clearCache()、unregisterAll()),并在合适时机(配置刷新、模块卸载、请求结束)调用
- 把强引用换成 WeakReference / SoftReference(适合缓存),或换用 Caffeine 等带淘汰策略的容器
- 上线后重复之前监控步骤:jstat 观察老年代趋势、jmap -histo 对比实例数、必要时再 dump 一次看引用链是否已断
Java免费学习笔记:立即使用
解锁 Java 大师之旅:从入门到精通的终极指南










