ThreadLocal 的实现原理:每一个 Thread 维护一个 ThreadLocalMap,key 为使用弱引用的 ThreadLocal 实例,value 为线程变量的副本,这些对象之间的引用关系如下:
实心箭头表示强引用,虚心箭头表示弱引用
ThreadLocal 的内存泄露发生在 Entry 上,我们现在来详细分析 Entry。
对于 Entry 的 key 来说,它是 ThreadLocal 对象,它有两个引用源,一个是栈内存上的 ThreadLocal Ref,一个是 Entry 中的 key,如下:
对于 Entry 的 value 而言它就只有一条引用链:
对于 Entry 来说,由于存在 key 和 value 两个引用路径,所以这里就会有两种情况:
栈上的 ThreadLocal Ref 不再使用了,但是由于 ThreadLocal 对象还有一条引用链存在,这就会导致它无法被回收,时间久了就会导致内存泄露。
由于线程池的存在,会让线程一直被重复利用,就会导致第二条 value 链一直存在,导致 ThreadLocalMap 无法被回收,从而导致内存泄露。
内存泄露的解决方案
第一种情况:弱引用
栈上的 ThreadLocal Ref 不再使用了,但是由于 ThreadLocal 对象还有一条引用链存在,这就会导致它无法被回收,时间久了就会导致内存泄露。
为了解决这种情况,ThreadLocal 使用了弱引用。即上图用虚线表示的部分。源代码如下:
tatic 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;}
}
...
}
对 JVM 熟悉的小伙伴知道,如果一个对象只具有弱引用,那么这个对象就会被垃圾回收器回收掉(被弱引用所引用的对象只能生存到下一次GC之前,当发生GC时候,无论当前内存是否足够,弱引用所引用的对象都会被回收掉)。
当堆栈上的 ThreadLocal Ref 不再使用了,ThreadLocal 对象就只有弱引用了,那么 ThreadLocal 对象就可以在下次GC时被回收掉了。
第二种情况:使用完 ThreadLocal 后调用 remove()
由于线程池的存在,会让线程一直被重复利用,就会导致第二条 value 链一直存在,导致 ThreadLocalMap 无法被回收,从而导致内存泄露。
value 的生命周期与 Thread 是一样的,由于线程池的存在,导致线程一直都被重复利用,从而导致 value 对象一直都无法被释放。那怎么解决呢?
我们知道 ThreadLocal 本身其实是不存储数据的,数据都是存在 ThreadLocalMap 中的,所以我们在每次调用ThreadLocal的get()、set()、remove() 方法的时候,内部实际会调用ThreadLocalMap的get()、set()、remove() 操作。而 ThreadLocalMap 的这些方法都会清理key为null,但是value还存在的Entry。
所以,当我们在一个 ThreadLocal 用完之后,可以手动调用一下remove(),就可以在下一次GC的时候,把Entry清理掉。
总结
由于 Thread 中包含了 ThreadLocalMap 的变量,因此 ThreadLocalMap 与 Thread 的生命周期是一样长,如果都没有手动删除对应key,都会导致内存泄漏。
但是,由于 ThreadLocal 使用了弱引用,则多了一层保障:弱引用 ThreadLocal 不会内存泄漏,因为它会在下一次 GC 时被回收。
ThreadLocal 内存泄漏的根源是:Thread 被重复利用,导致 value 强引用链一直存在,而导致内存泄露,注意,不是因为弱引用。
所以,当我们用完一个 ThreadLocal 后,可以手动调用一下remove(),就可以在下一次GC的时候,把Entry清理掉。
为什么我们在使用 ThreadLocal 的时候,一再强调要手动调用 remove() 方法。