ThreadLocal内存泄漏和弱引用分析
ThreadLocal实现原理
ThreadLocal实现原理是由每一个Thread维护一个ThreadLocalMap映射表,key是threadLocal对象,并且使用的是弱引用。看一下ThreadLocal的set方法:
public void set(T value) {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
createMap(t, value);
}
void createMap(Thread t, T firstValue) {
t.threadLocals = new ThreadLocalMap(this, firstValue);
}
直接是new一个ThreadLocalMap并赋值给当前线程的threadLocals属性。映射表由每一个线程自己维护,从而实现了线程之间的数据隔离。ThreadLocal在Java内存中的引用关系示意图如下:
内存泄漏分析
从上图可以看出,当ThreadLocal没有被GC Roots连接,因为弱引用的特点,不管当前内存空间足够与否,在GC时ThreadLocal都会被回收,这样会导致ThreadLocalMap中出现key为null的Entry,外部将不能获取这些key为null的Entry的value,并且如果当前线程一直存活(如线程池),那么就会导致value对应的Object一直无法被回收,产生内存泄露。
查看源码会发现,ThreadLocal的get、set和remove方法都实现了对所有key为null的value的清除,但仍可能会发生内存泄露,因为可能使用了ThreadLocal的get或set方法后发生GC,此后不调用get、set或remove方法,为null的value就不会被清除。
解决办法是每次使用完ThreadLocal都调用它的remove()方法清除数据,或者按照JDK建议将ThreadLocal变量定义成private static,这样就一直存在ThreadLocal的强引用,也就能保证任何时候都能通过ThreadLocal的弱引用访问到Entry的value值,进而清除掉。
为什么使用弱引用
从表面上看内存泄漏的根源在于使用了弱引用。网上的文章大多着重分析ThreadLocal使用了弱引用会导致内存泄漏,但是另一个问题也同样值得思考:为什么使用弱引用而不是强引用?
我们先来看看官方文档的说法:
To help deal with very large and long-lived usages, the hash table entries use WeakReferences for keys.
是为了应对长时间的使用场景,哈希表使用弱引用的 key。
下面我们分两种情况讨论:
- 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就会导致内存泄漏,而不是因为弱引用。