ThreadLocal内存泄漏的真正原因

Posted 小猪快跑22

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了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();
        

以上是关于ThreadLocal内存泄漏的真正原因的主要内容,如果未能解决你的问题,请参考以下文章

ThreadLocal 内存泄露的根本原因

ThreadLocal内存泄漏真因探究(转)

ThreadLocal 和内存泄漏

ThreadLocal 内存泄漏 代码演示 实例演示

漫聊 ThreadLocal (内存泄漏,弱引用)

ThreadLocal 搭配线程池使用造成内存泄漏的原因和解决方案