深入JDK源码系列--ThreadLocal内存泄漏问题

文章开始先解释一下内存泄漏和内存溢出,内存泄漏是由于不当操作(不当代码)使得某些内存无法被操作(回收),导致JVM可使用的内存莫名减少,大量的内存泄漏就会导致内存溢出。内存溢出:我们所需要的内存大于JVM所拥有的内存。言归正传,今天主要是来填坑的,上边文章讲了ThreadLocal部分源码,但是漏了一个remove()和不当操作ThreadLocal导致内存泄漏没有讲。
线程中有个ThreadLocalMap对象,这个对象就是来保存本地变量的,其key就是ThreadLocal对象而value就是其对应的值的。重点我们来看一下这个ThreadLocalMap类,这个类是ThreadLocal的内部类,其数据结构是个Entry对象,重点看这个Entry对象,它的key就是是ThreadLocal对象,而这个key采用的是弱引用,它的value就是ThreadLocal对应的值,而这个value是个强引用,JVM是不会回收value对象的,除非线程结束,对应的线程被销毁回收,value才会被回收。

static class Entry extends WeakReference<ThreadLocal<?>> {
    /** The value associated with this ThreadLocal. */
    Object value;
    //由于Entry中key是弱引用,当threadLoad对象为null,
    //由于采用了弱引用,
    //线程虽然key只向了threadLocal对象,
    //但是内存不够时依旧会回收ThreadLocal对象,
    //由于ThreadLocal对象被回收所以是的Value访问不了导致了内存泄漏。
    //内存泄漏:由于某些内存访问不了,导致JVM能使用的内存减少了
    Entry(ThreadLocal<?> k, Object v) {
        super(k);
        value = v;
    }
}

首先我们来假设一种情况,当我们在线程中使用了一个ThreadLocal对象,当我们使用完后,将这个ThreadLocal对象的引用置为null,此时我们堆内存中的ThreadLocal对象只有线程中的ThreadLocalMap中的一个弱引用指向它,当JVM进行垃圾回收时会将这些弱引用回收,由于我们的value是强引用是不会被回收的,此时我们无法就访问这些被回收的ThreadLocal对应的value了,而JVM也回收不了这些value,此时就造成了内存泄漏了。空口无凭,我们来模拟一下。
模拟代码如下:

public class SimulateMemoryLeak {
    private static final int TASK_SIZE=4;
    static final ThreadPoolExecutor THREAD_POOL=new ThreadPoolExecutor(TASK_SIZE,TASK_SIZE,10, TimeUnit.SECONDS,new ArrayBlockingQueue<>(10));
    static class ThreadLocalOOM{
       private byte[] bytes=new byte[1024*1024*5];
    }
    ThreadLocal<ThreadLocalOOM> threadLocal;
    private static final CountDownLatch COUNT_DOWN_LATCH=new CountDownLatch(TASK_SIZE);
    public static void main(String[] args) throws InterruptedException {
        for (int i=0;i<TASK_SIZE;i++){
            THREAD_POOL.execute(()->{
                COUNT_DOWN_LATCH.countDown();
                try {
                    COUNT_DOWN_LATCH.await();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                SimulateMemoryLeak simulateMemoryLeak = new SimulateMemoryLeak();
                simulateMemoryLeak.threadLocal=new ThreadLocal<>();
                simulateMemoryLeak.threadLocal.set(new ThreadLocalOOM());
                simulateMemoryLeak.threadLocal=null;
            });
        }
        System.out.println("=========>end");
    }

}

我们采用四个线程去模拟,假设里面存在一个大对象,这个大对象5M左右,我们将这个大对象放入线程中的ThreadLocalMap当中,并且我们没有其他引用指向ThreadLocal对象,我们采用JVisualVM工具监测一下内存变化。具体如图所示:
在这里插入图片描述
如图显示,我们确实是造成了内存泄漏,看这些锯齿状表示发生了垃圾回收,但是已经不能把这些大对象回收掉。如果此时我们再创建一些大对象就会造成OOM了。我们要避免这个问题就得采用ThreadLocal中的remove()方法了,其作用就是将用完的ThreadLocal对应的Entry对象移除ThreadLocalMap中。我们将修改后的代码再进行监控看一下
在这里插入图片描述
结果很明显,解决了内存泄漏问题。大致讲一下remove()主要原理吧:首先它会从线程获得ThreadLocalMap对象,然后从ThreadLocalMap(其实底层是一个Entry数组)找到ThreadLocal对应的Entry对象,具体是通过hash算法计算出ThreadLocal对象对应的hash值,这个值和ThreadLocalMap 中的数组长度-1相与就是可能的下标志,由于可能存在hase冲突,所以不一定是,所以需要通过for循环向后找,直到找到对应的Entry对象,然后将Entry对应置空并且移出数组。当JVM进行垃圾回收时就会将原来的Entry对象在堆内存开辟的空间回收调。
核心代码见java.lang.ThreadLocal.ThreadLocalMap中的remove()方法。

private void remove(ThreadLocal<?> key) {
    Entry[] tab = table;
    int len = tab.length;
    //计算下标
    int i = key.threadLocalHashCode & (len-1);
    //由于可能存在hash冲突,所以需要向后寻找
    for (Entry e = tab[i];
         e != null;
         e = tab[i = nextIndex(i, len)]) {
        if (e.get() == key) {
            //将引用置为空
            e.clear();
            //将Entry移出数组
            expungeStaleEntry(i);
            return;
        }
    }
}
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值