1、简单说明
线程Thread内维护一个map,底层为Entry[],entry的key为对ThreadLocal对象的弱引用,value为threadLocal.get()的值
当使用threadLocal.set("")时,set方法内将对threadLocal自身的弱引用作为map内Entry的key,存入当前线程的map中
2、错误的泄露归因及样例
创建很多线程,只set 不remove,归因为垃圾回收“无法回收”
为什么错误?
从定义上说:ThreadLocal为线程生命周期内,线程内共享变量用。 那么只要线程没结束,set在threadLocal中的变量不被回收是合理且正常的现象,通过创建很多线程并存入变量,来制造oom说是内存泄漏是莫名其妙的,因为本身就不该回收。
从代码上说:下文会讲,不存在泄露,只是常规用法的不可达,不是gcroot不可达。 所谓的记得remove避免内存泄漏,实质上是记得remove避免资源浪费,线程生命周期没结束就不释放,线程生命周期结束的话,线程下的map自然会一同被回收掉,怎么能叫泄露呢。
3、正确的泄漏原因及样例
网图
线程内threadLocalMap内部维护Entry[], Entry继承弱引用类WeakReference,弱引用threadLocal实例
当我们threadLocal=null;后,下次垃圾回收会回收只被弱引用的对象,之后entry.get()将为null(k被回收了)。但这并不是泄露的原因,很多网络说法看见kv就说k弱引用被回收了就泄露了,这是错误的,泄露原因在下图
因为我们已经threadLocal=null;(图中key),所以我们再也无法调用threadLocal的get方法最终走图中的getEntry方法,计算 i 然后访问到Entry[]中的entry中的value变量,而entry依旧在Entry[]中不会被回收。
说到底,泄露根本不是弱引用的问题,而是计算数组下标用的threadLocal被你设为null了 还不提前释放资源,导致以后就一直存在在数组内,这才泄露的。 甚至根本不该叫内存泄漏,因为gcroot可达,只是一直被数组持有,而逻辑设计上的通过threadLocal实例计算下标,导致一旦释放threadLocal,就无法计算到以前的下标,造成了伪泄露。
泄露代码样例
@SneakyThrows
public static void main(String[] args) {
List<byte[]> something = new ArrayList<>();
Thread t = new Thread(() -> {
ThreadLocal threadLocal = new ThreadLocal();
threadLocal.set("anything");
//注掉这行观察区别
threadLocal = null;
try {
Thread.sleep(600000);
} catch (InterruptedException e) {
e.printStackTrace();
}
});
t.start();
Thread.sleep(1000);
System.gc();
while (true) {
something.add(new byte[1024 * 1024 * 32]);
}
}
虚拟机参数添加-XX:+PrintGCDetails,在while内部打断点,gc后查看线程t下的threadLocalMap的情况,可以看到弱引用存在区别,但都是可达的,根本不叫内存泄漏。