threadlocalmap中entry的key使用weakreference是为了避免threadlocal对象因线程长期存活而无法被gc回收;但value仍为强引用,若不调用remove(),会导致内存泄漏。

ThreadLocalMap 中 Entry 的 key 为什么是 WeakReference?
因为 ThreadLocal 实例本身必须能被 GC 回收,否则只要线程还活着(比如线程池里的核心线程),ThreadLocalMap 就一直持有对它的强引用,导致该 ThreadLocal 对象永远无法释放——哪怕业务代码里早已不再持有它的任何强引用。
Entry 继承 WeakReference<threadlocal>></threadlocal>,就是把 ThreadLocal 实例作为弱引用对象传入。这样一旦外部所有强引用消失(比如局部变量出作用域、单例中字段被重置、Spring Bean 被销毁),下一次 GC 时,这个 key 就会被自动清空,Entry.key 变成 null。
但注意:这只是 key 的回收,value 仍是强引用,不会跟着一起被回收。这就是泄漏的起点。
WeakReference 的回收时机:不是“一设 null 就立刻消失”
WeakReference 的回收完全由 GC 触发,且只发生在 GC 过程中。它不依赖你是否调用了 threadLocal = null,也不依赖你是否执行了 remove() —— 那些只是辅助手段。
常见误解:
-
threadLocal = null后,Entry.key 立刻变null→ 错。只是断开了一个强引用,key 是否被回收取决于下一次 GC 是否发生、以及是否扫描到该弱引用 - 没调用
remove()就一定泄漏 → 不完全对。如果 ThreadLocal 实例本身已被 GC(key 为null),而 value 又是轻量对象(比如String、Integer),那其实危害有限;但如果 value 是大 Map、缓存对象、数据库连接等,就真会卡住内存 - 只要线程结束,ThreadLocalMap 就自动销毁 → 错。线程池中的线程不结束,
ThreadLocalMap就一直存在,里面所有未清理的 value 都被钉死
复现 OOM 的关键三步:让 WeakReference “失效”
要真正观察到 WeakReference 的回收行为和泄漏后果,得构造一个“key 被回收但 value 滞留”的场景。典型做法:
- 用固定大小线程池(如
Executors.newFixedThreadPool(2)),确保线程长期存活 - 在每个任务中创建一个新的
ThreadLocal<byte></byte>,并 set 一个 10MB 的byte[]—— 注意:这个ThreadLocal是局部变量,任务结束后无强引用 - 不调用
remove(),也不让ThreadLocal成为静态或实例字段(避免人为延长其生命周期)
运行几十轮后,dump 堆内存,你会看到大量 Entry 的 key 是 null,value 却是 10MB 的 byte[] 数组 —— 这就是 WeakReference 已生效(key 被回收)、但 value 因强引用滞留导致的泄漏证据。
为什么 remove() 不能省?WeakReference 不是万能的
WeakReference 解决的是 key 的泄漏,不是 value 的。Entry.key 变 null 后,该 Entry 就成了“脏项”(stale entry)。ThreadLocalMap 在后续的 get()、set()、remove() 过程中,会顺带探测并清理这些 key == null 的 Entry —— 但前提是得有人触发这些方法。
如果你只 set 从不 get,或者线程长期空闲,这些脏项就一直堆着,value 永远不释放。所以:
-
remove()是主动清理,最可靠 - 放在
try-finally块里是最小成本的兜底方式 - 别指望 WeakReference 替你擦屁股:它只管 key,不管 value 的生死
真正的安全边界在于:WeakReference 让 key 的回收变得可能,而 remove() 让 value 的回收变得确定。两者缺一不可。










