ThreadLocal导致内存泄露这么大bug,JDK不解决?

提示:文章写完后,目录可以自动生成,如何生成可参考右边的帮助文档


前言

提到ThreadLocal,在网上你会看到很多说它有内存泄露的问题,提到的解决方案也很简单,就是用完ThreadLocal对象后,要调用remove();方法进行清除。那么这个问题是如何产生的呢?为什么remove就能解决呢?今天我们就来一探究竟!


一、演示一个OOM的案例

注意:修改JVM参数,设置小一点。-Xms20M -Xmx20M -Xmn10M

/**
 * @author KinYang.Lau
 * @date 2021/3/30 8:17 下午
 * -Xms20M -Xmx20M -Xmn10M
 */
public class ThreadLocalOOM {
    public static void main(String[] args) {
        System.out.println("main 线程开始...");
        for (int i = 0; i < 100; i++) {
            ThreadLocal<Byte[]> threadLocal = new ThreadLocal<>();
            ///  在当前线程存入一个value
            System.out.println("在线程:"+Thread.currentThread().getName()+" 添加一个threadLocal");
            threadLocal.set(new Byte[1024*1024]);
        }
        System.out.println("main 线程完美运行结束");
    }
}

运行结果:
在这里插入图片描述
上面的程序就做了一件事情,就是在主线程 main 中,循环添加ThreadLocal对象。

二、分析原因

1.直接说原因

分析:
程序很简单,就是new一个对象,然后给对象set一个 Byte数组。一次for循环结束,这个对象应该就可以被GC回收,所以这个循环不论多少次,都不会造成OOM。但是这里为什么会出现OOM呢?肯定是ThreadLocal对象有问题,难道一个for循环结束后,ThreadLocal 的对象不会被GC回收吗?
答案是:ThreadLocal对象会回收。但是导致OOM的不是ThreadLocal对象没有被及时回收,而是new的Byte数组,没有GC被回收。

2.看源码找原因

为什么我上面说,ThreadLocal对象被回收了,但是Byte数组没有被回收,那么这个Byte数组是被谁引用着而导致没有被回收呢?我先说答案:是当前的线程所引用,这里也就是main线程。

来看ThreadLocal的set方法:

	public void set(T value) {
		///  获取当前线程
        Thread t = Thread.currentThread(); 
        /// getMap 方法很简单:就是返回线程的threadLocals属性; 默认是null
        ThreadLocalMap map = getMap(t);
        if (map != null)
            map.set(this, value);
        else
            createMap(t, value);
    }
	ThreadLocalMap getMap(Thread t) {
        return t.threadLocals;
    }

然后我继续跟进 createMap方法,new 一个ThreadLocalMap对象

    void createMap(Thread t, T firstValue) {
        t.threadLocals = new ThreadLocalMap(this, firstValue);
    }

初始化一个Entry[] 数组,并new一个Entry对象,放入数组

   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);
   }

我们来看看,Entry是个什么?
Entry继承了WeakReference,是一个弱指针引用对象,里面的弱引用对象就是for循环中new出来的ThreadLocal对象,这里可以理解为key,同时把set进来的 Byte[] 对象,设置给Entry的 value属性,这里是强引用。

static class Entry extends WeakReference<ThreadLocal<?>> {
    /** The value associated with this ThreadLocal. */
    Object value;

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

到这里,我们可以缕清楚set()方法的整体思路:

通过set方法,把Byte[]数组对象和ThreadLocal对象本身,包装成一个Entry对象,然后放到当前线程(这里是main线程)的threadLocals属性里。
所以,执行完set方法后,是当前线程对象(这里是main线程)持有了Byte[]的强引用和threadLocal对象的一个弱引用。

执行set方法后

在这里插入图片描述

第一个for循环结束后,栈中的强引用消失, threadLocal对象,只有entry的一个弱引用,随时可能被gc回收掉。而byte[]却一直被entry对象强引用,不会被gc回收。

在这里插入图片描述

两次for循环后,当前线程里面会有越来越多的entry,每个entry都会有一个Byte[1024*1024] ,还有可能有threadLocal对象(不发生gc这个对象就还在,会被回收)。所以随着for循环的不断执行,当前线程会持有越来越多的Entry对象,而这些空间不能被gc回收,最终造成OOM。
在这里插入图片描述
所以要避免这种情况发生应该怎么做?
那就是for循环结束后,调用一下threadLocal的remove方法,将entry清空,让gc回收掉。

/**
 * @author KinYang.Lau
 * @date 2021/3/30 8:17 下午
 * -Xms20M -Xmx20M -Xmn10M
 */
public class ThreadLocalOOM {
    public static void main(String[] args) {
        System.out.println("main 线程开始...");
        for (int i = 0; i < 100; i++) {
            ThreadLocal<Byte[]> threadLocal = new ThreadLocal<>();
            ///  在当前线程存入一个value
            System.out.println("在线程:"+Thread.currentThread().getName()+
            " 添加一个threadLocal");
            threadLocal.set(new Byte[1024*1024]);
            /// 方法解决,将threadLocal remove
            threadLocal.remove();
        }
        System.out.println("main 线程完美运行结束");
    }
}


总结

仔细看下ThreadLocal内存结构就会发现,Entry数组对象通过ThreadLocalMap最终被Thread持有,并且是强引用。也就是说Entry数组对象的生命周期和当前线程一样。即使ThreadLocal对象被回收了,Entry数组对象也不一定被回收,这样就有可能发生内存泄漏。ThreadLocal在设计的时候就提供了一些补救措施:

  • Entry的key是弱引用的ThreadLocal对象,很容易被回收,导致key为null(但是value不为null)。
    所以在调用get()、set(T)、remove()等方法的时候,会自动清理key为null的Entity。
  • remove()方法就是用来清理无用对象,防止内存泄漏的,所以每次用完ThreadLocal后需要手动remove()

有些文章认为是弱引用导致了内存泄漏,其实是不对的。
假设把弱引用变成强引用,这样无用的对象key和value都不为null,反而不利于GC,只能通过remove(方法手动清理,或者等待线程结束生命周期。
也就是说ThreadLocalMap的生命周期由持有它的线程来决定,线程如果不进入terminated状态,ThreadLocalMap就不会被GC回收,这才是ThreadLocal内存泄露的原因。

  • 2
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Jianyang.liu

从来没收到过一分钱

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值