ThreadLocal内存泄漏?
在分析ThreadLocal导致的内存泄露前,需要普及了解一下内存泄露、强引用与弱引用以及GC回收机制,这样才能更好的分析为什么ThreadLocal会导致内存泄露呢?更重要的是知道该如何避免这样情况发生,增强系统的健壮性。
强引用与弱引用
强引用,使用最普遍的引用,一个对象具有强引用,不会被垃圾回收器回收。当内存空间不足,Java虚拟机宁愿抛出OutOfMemoryError错误,使程序异常终止,也不回收这种对象。如果想取消强引用和某个对象之间的关联,可以显式地将引用赋值为null,这样可以使JVM在合适的时间就会回收该对象。
弱引用,JVM进行垃圾回收时,无论内存是否充足,都会回收被弱引用关联的对象。在java中,用java.lang.ref.WeakReference类来表示。可以在缓存中使用弱引用。
实现原理
static class ThreadLocalMap {
static class Entry extends WeakReference<ThreadLocal<?>> {
/** The value associated with this ThreadLocal. */
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
...
}
ThreadLocal的实现原理,每一个Thread维护一个ThreadLocalMap,key为使用弱引用的ThreadLocal实例,value为线程变量的副本。这些对象之间的引用关系如下,
实心箭头表示强引用,空心箭头表示弱引用
ThreadLocal 内存泄漏的原因
线程池中,一个线程会不断的从队列中拿去任务执行(不发生异常的话),这个线程是会一直存活的,换句话说他的ThreadLocalMap一直都在。如果此时ThreadLocal作为run方法里的局部变量,且ThreadLocalMap的key不声明为弱引用(则为硬引用)。run方法跑完,threadLocal被回收,但是线程不会结束(因为是线程池的线程),ThreadLocalMap不会被回收,对应上一步的生成的Entry还是保留在ThreadLocalMap的数组中,只要Thread不死(ThreadLocalMap就还在),GC就无法回收那个Entry(key,value),然后key不为null,已经没办法判断是不是回收的了。
还是讨论ThreadLocal作为run方法里的局部变量,且ThreadLocalMap的key声明为弱引用。run方法跑完,threadLocal被回收,但是线程不会结束(因为是线程池的线程),ThreadLocalMap不会被回收,对应上一步的生成的Entry还是保留在ThreadLocalMap的数组中。等待某次GC后,Entry的key被回收,变成Entry(null,value),在remove源码中,有对key==null的Entry进行value=null的help GC的操作。在某次调用remove还能再补救一下,相比为硬引用,已经没办法补救了,已经识别不出来哪些是正在需要GC的了。
总结来说,在ThreadLocal为局部变量的时候,不声明为WeekReference,忘记remove了,然后ThreadLocal被回收后,再也没办法确认ThreadLocalMap这个认为是用完的Entry(key,value)节点是不是可以GC了。而弱引用的Entry的key,某次GC后,无用的Entry都被标记为(null, value),此时就能识别出来是泄露的内存。
为什么建议把ThreadLocal声明为static?
这样就保留了ThreadLocal为强引用了,因为他是GCRoot对象,永不可能回收,key(ThreadLocal对象)是不是弱引用已经无所谓了。因为ThreadLocal是静态变量,所以只存在一个ThreadLocal对象,即使是线程池的线程(假设不发生异常,永远存活),这个ThreadLocal对象也只会占用ThreadLocalMap一个Entry的坑位,换句话说,及时我不回收他,我声明的这个对象在一个线程里只占一个Entry的坑位,永不回收问题也不是很大。对比局部变量ThreadLocal,不回收,每跑一次run方法可能就生成一个Entry,只要不调用remove,内存就会一直泄漏下去。