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();  
  } 
 