ThreadLocal的内存泄露问题

 更新时间:2023年03月30日 09:46:06   作者:小孙的Blog  
这篇文章主要介绍了Java中ThreadLocal的内存泄露问题,以及为什么会出现内存泄漏,感兴趣的小伙伴可以参考阅读

ThreadLocal的内部实现

在每一个线程Thread对象中,都维护了一个ThreadLocalMap对象。

 ThreadLocalMap中又维护了一个k v 形式的Entry对象,key指向了当前ThreadLocal对象,value就是我们实际在ThreadLocal中存储的值。

注意,这里的Entry中的key存放是ThreadLocal的弱引用。

 实现指的是强引用,虚线指的是弱引用。

 其实际上,ThreaLocal本身是不存储值的,我们在使用其对应的set、get方法时,都是操作的其对应的ThreadLocalMap对象。

为什么会出现内存泄露?

从上述可以看到,在Entry中的key存储的ThreadLocal的弱引用

弱引用在发生GC时,就会被垃圾回收掉,具体可以参考JVM相关的知识。

所以,在当前线程正在运行的时候,发生GC时,在ThreadLocal对象没有被其它地方强引用时,key指向ThreadLocal的虚引用就会立即断开(被垃圾回收掉),这时,就会出现ThreadLocalMap中存在key为null的Entry,只要当前线程不结束,该ThreadLocalMap对象就会一直存在,永远无法回收,因为此时还存在一条强引用的链路,从图中也可以发现:

Current Thread Reference --> Current Thread --> ThreadLocalMap --> EntryValue --> Object

所以这个时候就造成了内存泄露。

Entry对象的key为什么要使用弱引用,有什么好处?

在上述所说的问题中,即使ThreadLocalMap中存在key为null的Entry,但是该Entry的value值并不会因为GC而被回收(value存本身就存着一个强引用的对象),所以就导致了该对象不会被回收掉而出现了内存泄露。

其实,ThreadLocalMap在设计时就考虑到了这个方面,它也采取了一些措施来避免这种key为null,而value不为null的对象占用内存,在我们调用ThreadLocal的set、get、remove方法时,都会将这些key为null的对象清空掉,避免因为这种情况而导致内存泄露。

这也就是为什么key要存储弱引用的原因。

假设如果存储的强引用,我们断开ThreadLocal Reference —> ThreadLocal的引用,会发现key强引用了ThreadLocal,导致该对象永远无法被GC。

但是,即使上述提供了避免内存泄露的措施,但是不能完全避免,比如以下的情况:

  • 分配了ThreadLocal对象,但是并没有执行其get、set、remove方法,导致不能有效的清除null对象;
  • 使用线程池的情况下,使用完ThreadLocal一定要使用remove方法即时清理,因为ThreadLocal是属于某个线程的,而在使用线程池的情况下,这些线程都是可重复利用、存活时间长的线程,如果在使用过程中不仅从即使的remove,那么不仅会造成内存泄露的问题,还会引发一些功能逻辑问题,比如,B请求可能和A请求分配到了线程池中的同一个线程,那么它们拿到的ThreadLocal就是一样的。

 set

cleanSomeSlots

get

关于弱引用的一些知识补充

学习的过程中想到了一个问题,弱引用会不会导致运行过程中GC清除key,导致找不到对应的value?

可能是当时对弱引用的理解不够熟,所以产生了这个问题,如下面的代码。

public class TestDemo {

    static ThreadLocal threadLocal = new ThreadLocal();

    public static void main(String[] args) {

        threadLocal.set("demo");
        System.gc();
        System.out.println(threadLocal.get());	// demo

    }
}

为什么还获取到值,不是说在发现一次GC,弱引用就会被清除掉吗?

糊涂了。

弱引用只有在该对象没有被其它地方强引用的时候,才会被GC。

上述的原因就是因为,很明显,ThreadLocal对象除了被key弱引用,还由一个Reference强引用指向它,所以肯定不会被GC。

如果是这样,那下一次GC,这个对象就被干掉了。

举一个简单的例子,帮助理解:

WeakReference<User> userWeakReference = new WeakReference<User>(new User("jack"));

System.out.println(userWeakReference.get() == null);  // false

System.gc();

System.out.println(userWeakReference.get() == null);  // true

很明显,GC的时候直接清除了这个弱引用对象。

 userWeakReference.get(), 如果此方法为空, 那么说明weakReference指向的对象已经被回收了。

WeakReference<User> userWeakReference = new WeakReference<User>(new User("jack"));
User jack = userWeakReference.get();
System.out.println(userWeakReference.get() == null);  // false

System.gc();

System.out.println(userWeakReference.get() == null);  // false

 当我们添加了一个强引用来指向它的时候,该对象并不会被gc清除(弱引用还在)。

 到此这篇关于ThreadLocal的内存泄露问题的文章就介绍到这了,更多相关ThreadLocal内存泄露内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!

相关文章

最新评论