ThreadLocal内存泄漏的真正原因

注意:
看这篇文章之前得对 ThreadLocal 有个大致的了解,不然看起来还是蛮吃力的。

一、内存泄漏是因为弱引用吗?

先说结果,内存泄漏的确是因为弱引用引起的,为什么呢?

先看下 ThreadLocalset 方法:

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

createMap 如下:

void createMap(Thread t, T firstValue) {
    // t 指的是当前线程,即调用 threadLocal.set方法所在的线程
    t.threadLocals = new ThreadLocalMap(this, firstValue);
}

可以看出ThreadLocal 的数据是存在 ThreadLocalMap,而 ThreadLocalMap 是线程Thread中的变量,所以可以做到线程的数据隔离。

ThreadLocalMap 中是以数组的形式存放数据 private Entry[] table,至于插入数据时产生的碰撞这里不作说明了,可能下一篇会说明。Entry 的数据结构如下:

static class Entry extends WeakReference<ThreadLocal<?>> {
    /** The value associated with this ThreadLocal. */
    Object value;
    Entry(ThreadLocal<?> k, Object v) {
        super(k); // key 为 ThreadLocal的弱引用
        value = v;
    }
}

我们把上面的引用链画下来,如下:
在这里插入图片描述
其中,虚线表示弱引用

下面来说下为什么弱引用会导致内存泄漏?

一般项目中使用线程都是利用线程池,而线程池中的线程可能一直存活,这就导致了当外部不再持有 ThreadLocal的引用的时候,发生GC,导致 ThreadLocal 对象被回收了。

Entry 中的 keyThreadLocal 对象被回收了之后,会发生 Entrykeynull 的情况,其实这个 Entry 就已经没用了,但是又无法被回收,因为有 Thread->ThreadLocalMap ->Entry 这条强引用在,这样没用的内存无法被回收就是内存泄露。

当然,如果不是线程池使用方式的话,其实不用关系内存泄漏,反正线程执行完了就都回收了,但是一般我们都是使用线程池。

但是,没关系,在作者设计 ThreadLocal 的时候已经考虑到这种情况了,所以在 ThreadLocalsetget、以及扩容的时候都会清理 key = null 的数据。但是最佳实践是当不再使用的时候,手动 remove 掉,如下:

void dosth {
 threadlocal.set("1234");
 try {
  // do sth ... 
 } finally {
  threadlocal.remove();
 }
}

既然弱引用会导致内存泄漏,那为什么不用强引用呢?

同样的,如果是强引用,如果线程一直在,那么就算外部不再持有 ThreadLocal 的引用,但是会存在如下的引用链 GCRoots -> 线程对象 -> ThreadLocalMap -> Entry -> Key(ThreadLocal),导致ThreadLocal 对象得不到回收。

看到这里,可能有人会说那线程被回收之后就好了呀。

重点来了!线程在我们应用中,常常是以线程池的方式来使用的,而线程池中的线程一般是不会被清理掉的,所以这个引用链就会一直在,那么 ThreadLocal 对象即使没有用了,也会随着线程的存在,而一直存在着!

可以通过如下的方法来看看弱引用的 ThreadLocal 是否被回收。

ThreadLocal<String> threadLocal = new ThreadLocal<>();
        threadLocal.set("我是在主线程中设置的值");
        // 如果不设置为null,那么由于有强引用持有 threadLocal,那么不会被回收
        threadLocal = null; // 断开 ThreadLocal 的强引用
        System.gc(); // 主动垃圾回收

        Thread curThread = Thread.currentThread();
        Class<? extends Thread> clz = curThread.getClass();
        Field field = null;
        try {
            field = clz.getDeclaredField("threadLocals");
            field.setAccessible(true);
            Object threadLocalMap = field.get(curThread);

            Class<?> tlmClass = threadLocalMap.getClass();
            Field tableField = tlmClass.getDeclaredField("table");
            tableField.setAccessible(true);
            Object[] arr = (Object[]) tableField.get(threadLocalMap);

            for (Object o : arr) {
                if (o == null) continue;
                Class<?> entryClass = o.getClass();
                Field valueField = entryClass.getDeclaredField("value");
                Field referenceField = entryClass.getSuperclass().getSuperclass().getDeclaredField("referent");
                valueField.setAccessible(true);
                referenceField.setAccessible(true);
                System.out.println(String.format("弱引用key:%s    值:%s", referenceField.get(o), valueField.get(o)) + " , thread >> " +Thread.currentThread().getName());
            }
        } catch (NoSuchFieldException | IllegalAccessException e) {
            e.printStackTrace();
        }
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值