详解ThreadLocal为何存在内存泄漏
ThreadLocal是Java中用于保证线程安全的一种措施,通过给每个线程分配一个专属的值存储空间,保证线程各自维护自己的变量,从而不会发生并发访问问题。
但是ThreadLocal是存在着内存泄漏风险的,如果使用不当,容易发生memory leak错误。
首先解释什么是内存泄漏。
内存泄漏memory leak :是指程序在申请内存后,无法释放已申请的内存空间,一次内存泄漏似乎不会有大的影响,但内存泄漏堆积后的后果就是内存溢出。
线程Thread对象中,每个线程对象内部都有一个的ThreadLocalMap对象。如果这个对象存储了多个大对象,则可能早出内存溢出OOM。
为了防止这种情况发生,在ThreadLocal的源码中,有对应的策略,即调用 get()、set()、remove() 方法,均会清除 ThreadLocal内部的 内存。
ThreadLocal的内部是ThreadLocalMap。ThreadLocalMap内部是由一个Entry数组组成。Entry类的构造函数为 Entry(弱引用的ThreadLocal对象, Object value对象)。因为Entry的key是一个弱引用的ThreadLocal对象,所以在 垃圾回收 之前,将会清除此Entry对象的key。那么, ThreadLocalMap 中就会出现 key 为 null 的 Entry,就没有办法访问这些 key 为 null 的 Entry 的 value。这些 value 被Entry对象引用,所以value所占内存不会被释放。若在指定的线程任务里面,调用ThreadLocal对象的get()、set()、remove()方法,可以避免出现内存泄露。
ThreadLocal对象被GC回收了,那么key变成了null。Map又是通过key拿到的value的对象。所以,GC在回收了key所占内存后,没法访问到value的值,因为需要通过key才能访问到value对象。另外,如图所示的引用链:CurrentThread – Map – Entry – value ,所以,在当前线程没有被回收的情况下,value所占内存也不会被回收。所以可能会造成了内存溢出。
弱引用只要继承WeakReference
因为上述的原因,在ThreadLocal这个类的get()、set()、remove()方法,均有实现回收 key 为 null 的 Entry 的 value所占的内存。所以,为了防止内存泄露(没法访问到的内存),在不会再用ThreadLocal的线程任务末尾,调用一次 上述三个方法的其中一个即可。
因此,可以理解到为什么JDK源码中要把Entry对象,用 弱引用的ThreadLocal对象,设计为key,那是因为要手动编写代码释放ThreadLocalMap中 key为null的Entry对象。
那么GC什么时候回收弱引用的对象?弱引用对象是存活到下一次垃圾回收发生之前对象。
综上:JVM会自动回收某些对象将其置为null,从而避免OutOfMemory的错误。弱引用的对象可以被JVM设置为null。我们的代码通过判断key是否为null,从而 手动释放 内存泄露的内存。
为什么要将ThreadLocal设计为弱引用?
答:因为弱引用的对象的生命周期直到下一次垃圾回收之前被回收。弱引用的对象将会被置为null。我们可以通过判断弱引用对象是否已经为null,来进行相关的操作。在ThreadLocalMap中,如果键ThreadLocal已经被回收,说明ThreadLocal对象已经为null,所以其对应的值已经无法被访问到。这个时候,需要及时手动编写代码清理掉这个键值对,防止内存泄露导致的内存溢出。
static class Entry extends WeakReference<ThreadLocal<?>> {
/** The value associated with this ThreadLocal. */
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
强引用: 不会被回收的内存。
软引用: 内部不足的时候回收的内存。
弱引用: 存活到垃圾回收前的内存。