WeakReference 本身不是陷阱,真正问题是Key被回收后Value仍被强引用锁死在ThreadLocalMap中,线程长期存活导致Value成为无法访问的内存泄漏对象。

WeakReference 本身不是陷阱,真正的问题在于:Key 被回收后,Value 仍被强引用锁死在 ThreadLocalMap 里,而线程又长期不退出——这时 Value 就成了“活着但没人认得”的孤儿对象,持续占内存。
为什么 Key 设为弱引用,反而暴露了 Value 泄漏?
Key 用弱引用,是为了让 ThreadLocal 实例能及时被 GC 回收。比如你写了 threadLocal = null,若 key 是强引用,ThreadLocal 就会被 ThreadLocalMap 死死拽住,永远无法释放——那才是更隐蔽的泄漏。弱引用把问题“提前显形”:key 变 null 了,说明 ThreadLocal 已失效;但 value 还在 entry 里牢牢站着,外部完全访问不到它,GC 也清不掉它。
Key=null 后,Value 为何还不能被回收?
因为 ThreadLocalMap.Entry 的 value 字段是普通强引用,不是 WeakReference 或 SoftReference。只要 entry 对象本身还存在(而它属于线程的 threadLocals,线程不死,map 就不销毁),value 就一直可达。JVM 的可达性分析会顺着这条链走通:
Thread → ThreadLocalMap → Entry → value
所以 value 永远不算“垃圾”,哪怕 key 是 null、业务代码再也碰不到它。
线程池场景为何最危险?
- 核心线程长期存活,ThreadLocalMap 持续驻留堆中
- 每次任务 set() 一个大对象(如 byte[10MB]),却不 remove(),100 次就是 1GB 垃圾
- 这些 value 不仅自己占内存,还可能持有 Connection、Buffer、InputStream 等资源,引发连锁泄漏
- GC 日志看不出明显大对象,old gen 却缓慢上涨,OOM 前几乎无预警
清理机制为什么靠不住?
ThreadLocal 确实在 set()/get()/remove() 中做了“尽力清理”:扫描部分槽位,清除 key==null 的 entry。但这只是被动、局部、概率性的:
- 一个任务只调用一次 set(),之后再没触发任何 ThreadLocal 方法 → 清理只发生一次,且只扫一小段
- 线程空闲时什么也不做 → 零清理触发
- map 不扩容、不 rehash、不遍历 → 大量 stale entry 沉睡多年
它不是兜底方案,而是辅助手段。指望它自动清干净,等于把内存安全交给运气。

















