ThreadLocal之内存泄漏

发布时间 2023-05-28 19:12:17作者: JaxYoun

ThreadLocal之内存泄漏

前言

ThreadLocal机制是通过线程独占访问变量的方式,来解决并发安全问题的,每一个线程对象拥有独立的ThreadLocalMap容器,用来存储value,就此解决了线程隔离问题;
更宏观的讲,也是一种通过空间换时间,来提高程序执行效率的方式。

一、机制

1.1 ThreadLocal相关的定义

// 1.lang包中定义ThreadLocal
java.lang.ThreadLocal

// 2.在ThreadLocal中定义ThreadLocalMap内部类
java.lang.ThreadLocal.ThreadLocalMap

// 3.在ThreadLocalMap中定义内部类Map
java.lang.ThreadLocal.ThreadLocalMap.Entry

public class ThreadLocal<T> {

    static class ThreadLocalMap {

        static class Entry extends WeakReference<ThreadLocal<?>> {  // 这里的弱引用,其实不是内存泄漏的原因;不调用remove方法才是
            Object value;
            Entry(ThreadLocal<?> k, Object v) {
                super(k);
                value = v;
            }
        }

    }

}
  
// 4.Thread类中声明了类型为ThreadLocalMap的变量threadLocals
public class Thread implements Runnable {
	ThreadLocal.ThreadLocalMap threadLocals = null;
}

通过上述类的定义,得出其间的持有关系,可以推论出这几个关键对象间的强引用关系:

Thread -> ThreadLoacalMap -> Entry -> value

从上述对象的强引用关系,可以推断出,在不手动remove的情况下,它们间的生命周期关系:

Thread == ThreadLoacalMap == Entry= = value

1.2 调用remove方法的重要性

ThreadLocal.remove()会将整个Entry对象删除:
手动remove:此后Entry对value的强引用也就断掉了,value就能顺利被GC回收了。
不手动remove:Entry将一直存活,Entry对value的强引用也将一直存在,value也将随之存活;但此时,存活的value却没有任何地方使用,白白占用空间,就形成了泄漏。
假如以线程池等方式来管理目标线程,假如目标线程一直得不到回收,其生命周期势必很长。每调用一次set方法,就多一个泄漏的value,长此以往,被浪费的内存量会很大。

三、但也不必就此因噎废食,就不用ThreadLocal,因为JVM已对此进行了一系列优化,降低损失

  1. 调用set方法时,会触发采样清理、全量清理,扩容时还会继续检查。
  2. 调用get方法时,若未直接命中、或发生了向后环形查找,也会触发清理。
  3. 调用remove方法时,除了会清理命中的Entry,还会触发向后清理。

注意:网上很多人提到Entry中key是弱引用这个点
我顺着这个思路去推导,假如把这个引用改为强引用,若不remove,该泄漏的还是会泄漏;
私以为,正确的是,应该从对象间强引用关系生命周期关系的角度来讨论这个问题。
所以,将内存泄漏的锅扔给弱引用,完全是一些人看到陌生或不常用的机制,没有深入理解,就枉然地错误归因,希望不要被这些声音误导

  1. 强引用:只要有引用,都不会被回收。
  2. 软引用:内存溢出前,进行回收,常用来做缓存。
  3. 弱引用:不论内存是否会溢出,遇到GC就被回收。
  4. 虚引用:等同于无引用,对象被回收时,会收到一个系统通知。