threadlocal内存飙升本质是线程池中threadlocalmap的entry未清理,导致value强引用对象长期驻留堆中;须用jmap抓堆快照,mat中统计非null entry数量、查key为null的幽灵entry,并检查web过滤器、线程池任务及异步回调中remove是否全覆盖。

ThreadLocal 内存占用持续飙高,本质是线程池中 ThreadLocalMap 的 Entry 没被清理,导致 value 强引用的对象长期驻留堆中。排查不能只看 ThreadLocal 实例数,关键要定位到每个线程里“残留的、未 remove 的 value”。
第一步:用 jmap 抓堆快照,聚焦 ThreadLocalMap
在内存飙升时立即执行:
-
jmap -dump:format=b,file=heap.hprof
—— 生成堆转储 - 用 Eclipse MAT 或 JProfiler 打开,搜索 java.lang.ThreadLocal$ThreadLocalMap
- 展开其 table 字段,统计非 null Entry 的数量(不是 ThreadLocal 对象个数)
- 右键某个 Entry → “Path to GC Roots” → 看 value 是什么类型、多大(比如一个 200KB 的 RequestContext)
第二步:查 Entry 的 key 是否为 null(幽灵 entry)
MAT 中用 OQL 快速筛查无效 Entry:
- SELECT * FROM java.lang.ThreadLocal$ThreadLocalMap$Entry WHERE this.key = null
- 如果结果大量存在,说明 ThreadLocal 实例已被 GC 回收(key 弱引用失效),但 value 还卡着没释放——这是典型泄漏信号
- 再结合 value 类型判断是否业务对象(如 UserSession、TraceContext、大型缓存 Map)
第三步:回溯代码,确认 remove 是否可靠执行
重点检查三类场景:
-
Web 请求链路:Filter 或 Spring Interceptor 的
doFilter()/afterCompletion()中是否调用了threadLocal.remove() -
线程池任务:
ExecutorService.submit(Runnable)的 run() 方法末尾是否有try-finally { ... remove() } - 异步回调:CompletableFuture.thenApplyAsync() 等跨线程操作,是否在回调开始前 set、结束后 remove;有没有遗漏分支(比如异常路径没清理)
第四步:验证清理逻辑是否覆盖所有出口
ThreadLocal 的生命周期必须与线程使用周期对齐。常见疏漏:
- 只在正常流程 remove,但 catch 块里没写,异常后 value 残留
- 用了 @Async 注解,但没配 TaskDecorator,导致子线程无法继承或清理上下文
- ThreadLocal 初始化用了 lambda 或匿名内部类,隐式持有了外部类 this,使整个 service 实例无法回收
不复杂但容易忽略
Java免费学习笔记:立即使用
解锁 Java 大师之旅:从入门到精通的终极指南











