Java ThreadLocal内存泄漏分析
原理参考: ThreadLocal原理以及用法详解-CSDN博客
ThreadLocal使用完都建议调用remove()清除上下文,特别是在线程池的场景。如果不这样做,可能会造成内存泄露。
我们来一步一步分析下是如何造成的。
一,ThreadLocal在内存里怎么存的?
首先,Thread、ThreadLocal、目标对象在JVM中是这样存储的:

这里的Map其实不是常规的K/V存储的Map,具体在代码里如下图:

所以Key其实就是referent属性,该属性定义在WeakReference的父类里:


二,理解弱引用
当对象仅被弱引用引用时,垃圾回收器会在 下一次回收周期中回收该对象,无论当前内存是否充足。
Object obj = new Object();
WeakReference<Object> weakRef = new WeakReference<>(obj);
// 还有强引用,目标对象(obj指向的对象)不会回收
System.gc();
System.out.println(weakRef.get()); // 不为null// 断开强引用
obj = null;
System.gc();
// 目标对象将被回收
System.out.println(weakRef.get()); // null
特别注意的是,这里涉及2个对象:
- 目标对象的回收由弱引用机制保证。
- WeakReference对象本身 的回收由普通 GC 规则决定,即如果还有它的强引用(如全局变量、集合),这个对象不会回收。

三,内存泄露如何发生?

内存泄露前提:不执行threadLocal.remove(),线程也不回收(如线程池核心线程)。
上图是典型内存泄露场景。
解释下,ThreadLocal没有强引用,所以只会在执行set(obj)之后,被当做 弱引用目标包装在ThreadLocalMap的Entry中。
手动执行GC后,ThreadLocal就被回收了,referent也就是null了。但value仍然以强引用引用着MyObject,也就不会释放和回收了。
内存泄漏的积累:
如果线程执行多次任务,创建了多个局部变量ThreadLocal且未清理,ThreadLocalMap 中可能会积累大量key=null的无效Entry,最终导致严重的内存泄露。
虽然ThreadLocalMap在调用get、set、remove时会扫描并移除key=null的Entry,但如果长期不调用这些方法,无效Entry还是会持续堆积。
所以建议ThreadLocal被定义成常量:
private static final ThreadLocal<Object> threadLocal = new ThreadLocal<>();
但这个并不影响内存泄露的发生,即threadLocal是否被弱引用回收不是决定是否内存泄漏。
四,内存泄露如何避免?
始终调用 remove(),且尽量避免存储大对象。
try {
threadLocal.set(data);
// 执行业务逻辑...
} finally {
threadLocal.remove();
}