线程池本身不泄漏内存,但用错会拖住业务对象;重点排查老年代内存持续上涨、worker线程数居高不下、任务队列size增长及其中runnable持有大对象等现象。

线程池本身不泄漏内存,但用错就容易拖着大量对象不放——排查重点不是线程池对象,而是它“卡住”的那些业务对象。
先看运行时有没有泄漏迹象
别急着开工具,先盯住几个关键信号:
- 老年代内存持续上涨,Full GC 后回收极少,且每次 GC 后基线比上次更高
- jstack 查到 ThreadPoolExecutor$Worker 线程数长期不降,或稳定维持在高位
- 监控发现任务队列(如 LinkedBlockingQueue)size 持续增长,尤其队列里 Runnable 持有 DTO、文件流、缓存 Map 等大对象
- 应用响应变慢、GC 频次飙升、频繁出现 GC overhead limit exceeded
用 MAT 抓住“不肯放手”的引用链
确认异常后,立即导出堆 dump:
jmap -dump:live,format=b,file=heap.hprof
在 Java 中初始化和管理阿里云 SDK客户端。包括单例模式、线程安全、endpoint 与 region 配置、VPC 终端节点、同步与异步等。
在 MAT 中按以下路径深挖:
- 直搜 ThreadPoolExecutor 或自定义线程池类名 → 右键 → Merge Shortest Paths to GC Roots(勾选 exclude weak/soft references)→ 看哪些业务对象被强引用住
- 展开线程池实例 → 找 workQueue 字段 → 右键该队列 → List objects → with incoming references → 逐个检查队列中 FutureTask 的 callable 或 runnable 持有哪些大对象(比如 byte[]、UserVO、Spring Context)
- 搜 ThreadLocalMap → 看 table 数组中 value 是否指向业务对象 → 再逆向查是哪个 Worker 线程的 threadLocals 持有它(线程复用下未 remove 是高频坑)
盯紧三类典型错误用法
这些模式在 MAT 里往往一目了然:
- 无界队列 + 慢任务:new ThreadPoolExecutor(core, max, keepAlive, TimeUnit.SECONDS, new LinkedBlockingQueue()) → MAT 中 LinkedBlockingQueue.count 极高,每个 FutureTask 的 callable 持有 Service 实例或上下文 Map
- 任务对象自带强引用:Runnable 匿名内部类引用了外部 Activity、Service 或大缓存对象 → MAT 中该 Runnable 的 this$0 指向不该存活的对象
- ThreadLocal 未清理:线程池复用线程,任务中 set 了 ThreadLocal 但没调 remove() → MAT 中 ThreadLocalMap.table[i].value 指向业务对象,且该 entry 的 key 已为 null(说明 key 被回收,value 却还挂着)
代码层面快速验证与修复
定位到可疑对象后,回代码确认是否属于以下情况:
- 线程池是否用了 Executors.newCachedThreadPool() 或无界队列?应改用有界队列 + 明确拒绝策略(如 CallerRunsPolicy)
- 提交的任务是否在执行完后主动释放了持有的大对象(如 close 流、clear list、置空缓存引用)?
- 用了 ThreadLocal 的地方,是否在 finally 块或 try-with-resources 后明确调用了 remove()?
- 是否误将线程池或任务对象存入 static 集合、监听器列表等长生命周期容器中?
Java免费学习笔记:立即使用
解锁 Java 大师之旅:从入门到精通的终极指南










