ThreadLocal内存泄漏问题

上一篇做了个实验,不正确的使用ThreadLocal可能会导致内存泄漏,这一篇就来讲一下为什么ThreadLocal会导致内存泄漏。

ThreadLocal实现原理

v2-45affd67cf3dfb5637878d8f46ea5061_r

ThreadLocal的核心是一个ThreadLocalMap,ThreadLocalMap中保存了一个Entry数组,Entry继承了ThreadLocal的弱引用:ThreadLocalMap中元素的key为当前ThreadLocal对象,而value对应线程的变量副本,每个线程可能存在多个ThreadLocal。

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

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

ThreadLocal的操作:

  • set:获取当前Thread的ThreadLocalMap后,以当前ThreadLocal为key,设置的value为值,构建Entry并设置到ThreadLocalMap的Entry table中。其中Entry的key是弱引用了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);
}
  • get:获取当前线程的ThreadLocalMap,获取到以当前ThreadLocal为key的value并返回,即获取到当前ThreadLocal代表的局部变量。
  • remove:获取当前线程的ThreadLocalMap后,移除当前ThreadLocal为key的项目:其中expungeStaleEntry用于清除当前idx对应的Entry(设置key和value都为null),并且继续往后移动清除key为null的Entry项,知道遇到一个null。
private void remove(ThreadLocal<?> key) {
    Entry[] tab = table;
    int len = tab.length;
    int i = key.threadLocalHashCode & (len-1);
    for (Entry e = tab[i];
         e != null;
         e = tab[i = nextIndex(i, len)]) {
        if (e.get() == key) {
            e.clear();//这里是设置referent为null
            expungeStaleEntry(i);
            return;
        }
    }
}

内存泄漏问题

每个线程都有一个ThreadLocalMap,map中的可以以弱引用引用着ThreadLocal,假设当前ThreadLocal是局部变量(上一篇的情况),当方法退出后ThreadLocal置为null,这时候没有强引用指向ThreadLocal,则ThreadLocal会被回收,map中的key就变成了null。但是value不能被回收,因为存在一条从current thread连接过来的强引用。只有当前thread结束以后, current thread被销毁,强引用断开, Current Thread, Map, value将全部被GC回收。

综上所述:当ThreadLocal设为null,但是当前Thread没有被回收的情况下,是存在内存泄漏问题的。比如使用线程池的时候,线程结束是不会销毁的,会再次使用的就可能出现内存泄露。

解决方法也很简单,在ThreadLocal的set和remove中会调用expungeStaleEntry来清除key为null的Entry。所以每次使用完都调用remove即可。

为什么用弱引用

ThreadLocal为什么使用弱引用,可以对比来看:

To help deal with very large and long-lived usages, the hash table entries use WeakReferences for keys.

  • key 使用强引用:引用的ThreadLocal的对象被回收了,但是ThreadLocalMap还持有ThreadLocal的强引用,如果没有手动删除,ThreadLocal不会被回收,导致Entry内存泄漏。
  • key 使用弱引用:引用的ThreadLocal的对象被回收了,由于ThreadLocalMap持有ThreadLocal的弱引用,即使没有手动删除,ThreadLocal也会被回收。value在下一次ThreadLocalMap调用set,get,remove的时候会被清除。

比较两种情况,我们可以发现:由于ThreadLocalMap的生命周期跟Thread一样长,如果都没有手动删除对应key,都会导致内存泄漏,但是使用弱引用可以多一层保障:弱引用ThreadLocal不会内存泄漏,对应的value在下一次ThreadLocalMap调用set,get,remove的时候会被清除。

因此,ThreadLocal内存泄漏的根源是:由于ThreadLocalMap的生命周期跟Thread一样长,如果没有手动删除对应key就会导致内存泄漏,而不是因为弱引用。

ref:https://juejin.im/post/6844903683751149582
https://zhuanlan.zhihu.com/p/56214714

comments powered by Disqus