ThreadLocal 的 Entry 使用弱引用是为了在线程复用场景下避免内存泄漏,其 key 设为弱引用可被 GC 回收,而 value 仍强引用,需显式调用 remove() 清理。

ThreadLocal 的 Entry 为什么是弱引用
因为 ThreadLocal 的核心设计目标是:让每个线程持有自己的一份副本,且在线程结束时能自动清理。但 JVM 无法主动感知线程生命周期,所以 JDK 用弱引用来解耦 ThreadLocal 实例和 Entry 的生命周期——Entry 的 key 是 WeakReference<threadlocal></threadlocal>,value 却是强引用。
这意味着:只要外部不再持有该 ThreadLocal 实例(比如被置为 null 或超出作用域),key 就可能被 GC 回收,Entry 变成“key == null”的脏 entry。但 value 还活着,且线程的 ThreadLocalMap 仍持有它。
- 不是所有弱引用都安全;这里只弱了 key,没弱 value
- GC 不会主动扫描并清理整个
ThreadLocalMap,得靠后续的get/set/remove触发探测和清理 - 线程长期存活(如线程池中的 worker 线程)时,这个“脏 entry”就容易堆积
内存泄漏的真实触发场景
泄漏不是发生在 ThreadLocal 被创建时,而是当它被“遗弃”后,又没人调用 remove(),且线程持续复用 —— 典型于 Web 容器或自定义线程池。
例如 Tomcat 中一个请求用 ThreadLocal<connection></connection> 存数据库连接,但没在 filter 或 finally 里调用 threadLocal.remove();下个请求复用同一线程,旧的 Connection 对象就卡在 map 里出不去。
立即学习“Java免费学习笔记(深入)”;
- 静态
ThreadLocal字段最容易出问题:类加载器卸载不了,key 永远不为 null?错——静态引用本身不会阻止 key 被回收,但 value 引用的对象可能持有着 ClassLoader - 线程池中未清理的
ThreadLocal,value 若是大对象(如byte[]、缓存 Map),泄漏更明显 -
set(null)不能代替remove():它只是把 value 设为 null,Entry 还在,key 也没变,map size 不减
怎么正确清理 ThreadLocal
别依赖“线程结束自动清理”,线程池让这假设失效。必须显式调用 remove(),且时机要对。
- 在 try-finally 块末尾调用
threadLocal.remove(),不要只在 catch 里做 - 如果是 Servlet 过滤器,确保
doFilter()执行完后清理,哪怕发生异常也要清理 - 避免在 lambda 或匿名内部类里直接 capture
ThreadLocal并忘记清理(尤其在异步回调中) - 使用
InheritableThreadLocal更危险:子线程继承后若不清理,泄漏面更大
排查和验证泄漏的方法
光看代码逻辑不够,得看运行时行为。重点不是“有没有 remove”,而是“有没有被执行到”。
用 jstack + jmap 配合分析:jmap -histo:live <pid> 查看 ThreadLocalMap$Entry 数量是否随请求增长;再用 jmap -dump:format=b,file=heap.hprof <pid>,用 MAT 打开,按 incoming references 查哪些 Entry 的 key == null,再看它们的 value 持有哪些大对象。
-
ThreadLocalMap本身不会被频繁 GC,但它的 value 若是长生命周期对象,会拖慢老年代回收 - 某些监控工具(如 Arthas)可动态 attach 并执行
ognl '@java.lang.Thread@currentThread().threadLocals'查当前 map 内容 - 注意:JDK 9+ 对
ThreadLocalMap做了部分优化(如 resize 时顺便 clean),但不改变根本机制,该清理还得清理
真正难的不是理解弱引用,是在线程复用场景下,把清理逻辑嵌进所有可能的退出路径里——漏一条,就可能积少成多。










