【详解】ThreadLocal为什么会内存泄漏

1、ThreadLocal 真的会内存泄漏吗?

       在网上去学习ThreadLocal经常看到,在不使用的时候需要调用remove()方法,否则会有内存泄漏。通过查找资料和阅读源码并验证(验证)发现是不会出现内存泄漏。

      ThreadLocal存储将存储对象放置到Thread线程中,threadLocals 变量中,ThreadLocalMap是ThreadLocal的静态内部类

    /* ThreadLocal values pertaining to this thread. This map is maintained
     * by the ThreadLocal class. */
    ThreadLocal.ThreadLocalMap threadLocals = null;

     ThreadLocal存放数据,是将 threadLoacl 本身作为Key存放存在ThreadLocalMap中,value是ThreadLocal放置的值。

    public void set(T value) {
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        if (map != null)
            map.set(this, value);
        else
            createMap(t, value);
    }

    获取ThreadLocal存在对象

    public T get() {
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        if (map != null) {
            ThreadLocalMap.Entry e = map.getEntry(this);
            if (e != null) {
                @SuppressWarnings("unchecked")
                T result = (T)e.value;
                return result;
            }
        }
        return setInitialValue();
    }

内存泄漏论,有两个内存泄漏的点

①、ThreadLocal 如果一直存在,不被回收,与它关联的ThreadLocalMap中的存储对象时Entry会一直互相引用不被回收。

②、Thread线程如果一直存在,ThreadLocal不会被回收。

③、如果线程一直存在,Thread 中的ThreadLocalMap变量的Entry不能被GC回收掉的,虽然是entry的key是软引用会被回收掉,但是value不会被回收,如果Thread一直存在就会内存泄漏。

 

这里不得不叹服大师们武功的出神入化高深莫测,Josh Bloch 和 Doug Lea 使用弱引用巧妙的解决了前两个问题。

截取一部分 ThreadLocalMap的代码,可以看出使用了弱引用 WeakReference

    static class ThreadLocalMap {

        /**
         * The entries in this hash map extend WeakReference, using
         * its main ref field as the key (which is always a
         * ThreadLocal object).  Note that null keys (i.e. entry.get()
         * == null) mean that the key is no longer referenced, so the
         * entry can be expunged from table.  Such entries are referred to
         * as "stale entries" in the code that follows.
         */
        static class Entry extends WeakReference<ThreadLocal<?>> {
            /** The value associated with this ThreadLocal. */
            Object value;

            Entry(ThreadLocal<?> k, Object v) {
                super(k);
                value = v;
            }
        }
}

2、什么是弱引用?

弱引用通过WeakReference类实现。 弱引用的生命周期比软引用短。在垃圾回收器线程扫描它所管辖的内存区域的过程中,一旦发现了具有弱引用的对象,不管当前内存空间足够与否,都会回收它的内存。由于垃圾回收器是一个优先级很低的线程,因此不一定会很快回收弱引用的对象。弱引用可以和一个引用队列(ReferenceQueue)联合使用,如果弱引用所引用的对象被垃圾回收,Java虚拟机就会把这个弱引用加入到与之关联的引用队列中。
强引用、软引用、弱引用、幻象引用有什么区别?有哪些使用场景?

3、ThreadLoad使用弱引用,会不会被GC误回收

当然是不会的。上文提到ThreadLocalMap中的Entry 的key 是threadLocal对象本身,但是会被作为弱引用

     ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
            table = new Entry[INITIAL_CAPACITY];
            int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
            table[i] = new Entry(firstKey, firstValue);
            size = 1;
            setThreshold(INITIAL_CAPACITY);
        }
static class Entry extends WeakReference<ThreadLocal<?>> {
            /** The value associated with this ThreadLocal. */
            Object value;

            Entry(ThreadLocal<?> k, Object v) {
                super(k);
                value = v;
            }
        }

 上面是实现代码

4、为什么不会被回收掉呢?

因为虽然Entry的Key是弱引用,但是Key指向的是强引用,然后出现每次GC之前都会去尝试回收掉KEY,然后回收不掉。使用软引用将ThreadLocal对象和ThrealLocal.Entry对象的强引用关系 ,转变为弱引用关系。大家都知道强引用关系,GC标记是引用可达,他们之间有任何一个还在使用就不会被回收,换成弱引用之后,两个对象之间就不是强引用关系,可以各自保证被回收掉。

 

5、验证ThreadLocal会被GC回收

环境JDK8,idea,JDK自带工具jvisualvm.exe

①:Thread线程如果一直存在,ThreadLocal不会被回收

请看代码注释理解,

public class Test {

    public static void main(String[] args) {

        new Thread(() -> {
            //ThreadLocal放置到  代码块中,为证明 threadLocal 会被GC回收掉
            {
                ThreadLocalTest<AlarmEmailVO> threadLocal = new ThreadLocalTest<>();
                threadLocal.set(new AlarmEmailVO());
                System.out.println(threadLocal.get());
            }

            int i = 0;
            while (true) {
                try {
                    Thread.sleep(1000);
                    //每秒加10M 堆内存 能看出内存增长趋势
                    //GC时,a临时变量会被回收
                    int[] a = new int[1024 * 1024 * 10];
                    if (i++ > 100) {
                        break;
                    }
                    i++;
                } catch (InterruptedException e) {
                }
            }

        }).start();
    }
}

class AlarmEmailVO {
    @Override
    public String toString() {
        return "AlarmEmailVO--AlarmEmailVO";
    }
}

/**
 * 继承ThreadLocal 加入100M的变量,方便当发生GC时,通过堆内存曲线发现是否被回收掉
 *
 * @param <T>
 */
class ThreadLocalTest<T> extends ThreadLocal<T> {
    int[] bb = new int[1024 * 1024 * 100];
}

通过使用jvisualvm手动触发GC发现,ThreadLocall对象被回收了,因为不被回收的话,堆内存至少100M

②、ThreadLocal 如果一直存在,不被回收,与它关联的ThreadLocalMap中的存储对象时Entry会一直互相引用不被回收。

public class Test {

    public static void main(String[] args) throws InterruptedException {
        ThreadLocal<AlarmEmailVO> threadLocal = new ThreadLocal<>();
        threadLocal.set(new AlarmEmailVO());
        System.out.println(threadLocal.get());

        new Thread(() -> {
            int i = 0;
            while (true) {
                try {
                    //支持一段时间,便于观察对内存变化
                    Thread.sleep(1000);
                    if (i++ > 30) {
                        break;
                    }
                    i++;
                } catch (InterruptedException e) {
                }
            }
        }).start();

        Thread.sleep(10000);
    }
}

class AlarmEmailVO {
    //AlarmEmailVO 中的临时变量,为了证明Thread中的ThreadLocalMap 会被回收,对内存会有明细变化
    int[] bb = new int[1024 * 1024 * 100];

    @Override
    public String toString() {
        return "AlarmEmailVO--AlarmEmailVO";
    }
}

记得持续手动执行GC

③、如果线程一直存在,Thread 中的ThreadLocalMap变量的Entry不能被GC回收掉的

package com.sp;

public class Test {

    public static void main(String[] args) throws InterruptedException {

        new Thread(() -> {
            {
                ThreadLocalTest<AlarmEmailVO> threadLocal = new ThreadLocalTest<>();
                threadLocal.set(new AlarmEmailVO());
                System.out.println(threadLocal.get());

                try {
                    Thread.sleep(10000);
                } catch (InterruptedException e) {

                }
            }

            int i = 0;
            while (true) {
                try {
                    //支持一段时间,便于观察对内存变化
                    Thread.sleep(1000);
                    byte[] bb = new byte[1024 * 1024 * 20];
                    if (i++ > 30) {
                        break;
                    }
                } catch (InterruptedException e) {
                }
            }
        }).start();

        Thread.sleep(10000);
    }
}

class AlarmEmailVO {
    //AlarmEmailVO 中的临时变量,为了证明Thread中的ThreadLocalMap 会被回收,对内存会有明细变化
    byte[] bb = new byte[1024 * 1024 * 100];

    @Override
    public String toString() {
        return "AlarmEmailVO--AlarmEmailVO";
    }
}

class ThreadLocalTest<T> extends ThreadLocal<T>{

    byte[] bb = new byte[1024 * 1024 * 100];

}


Thread结束前,一直有100M内存,所以ThreadLocal的对象没有被回收掉

总结:

       ThreadLocal会出现内存泄漏,但是已经提供了 remove()方法ThreadLocal不用之后调用remove()方法,删除掉该对象。

 

推荐文章:

正确理解Thread Local的原理与适用场景 这是转发的文章

用弱引用堵住内存泄漏

 

 

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值