ThreadLocal内存泄漏问题解析
关于内存泄漏这个问题需要从ThreadLocal的设计开始讲起
1. ThreadLocal对于脏entry的定义及处理
下面是remove方法
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();
expungeStaleEntry(i);
return;
}
}
}
该方法逻辑很简单,通过往后环形查找到与指定 key 相同的 entry 后,先通过 clear 方法将 key 置为 null 后,使其转换为一个脏 entry,然后调用 expungeStaleEntry() 将其 value 置为 null,以便垃圾回收时能够清理,同时将 table[i]置为 null。从上面的分析可以知道ThreadLocal是通过remove方法将key置为null后进行清理。
2. key设置为弱引用的原因
下面是ThreadLocalMap.Entry的源码
static class Entry extends WeakReference<ThreadLocal<?>> {
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k);
this.value = v;
}
}
可以看到这个哈希表以ThreadLocal对象为key,且这个引用是一个弱引用,那么这个地方为什么要设计成弱引用呢?下面用反证法来分析这个问题:
- 假设这个引用是强引用,那么对于一个Threadlocal对象存在如下强引用链:Thread->ThreadLocal.ThreadLocalMap->Entry->key(ThreadLocal)、value。另外使用ThreadLocal的对象还持有当前ThreadLocal对象的强引用,假设现在使用ThreadLocal的对象被回收掉了,那么ThreadLocal对象还是被强引用,导致ThreadLocal对象无法被回收引发内存泄漏
解决上述问题的办法就是将Entry对key(ThreadLocal)的引用改为弱引用,这样在使用ThreadLocal的对象被回收后,ThreadLocal也就不存在被强引用,ThreadLocal对象被回收后该entry就变成了一个脏entry,而且在get、set、resize等方法中遇到脏entry都有调用expungeStaleEntry()进行处理。
那么为什么不把Entry或value也设计弱引用呢,这样在gc时直接就把Entry或value回收掉了
- 由于使用对象一般只会持有key的引用,如果把Entry或value也设计弱引用,可能会导致Entry或value在使用中被回收
3. 内存泄漏原因
经过上面的分析我们知道了key设计为弱引用的原因以及ThreadLoacal对于脏entry的处理,可以看到Josh Bloch和Doug Lea为了防止内存泄漏做了很多努力,但是为什么还会内存泄漏呢?
- 由于长时间没有调用get、set、resize、remove方法,导致脏entry无法及时被回收
4. 总结
为了防止内存泄漏,一定要在使用完ThreadLocal后及时调用remove方法进行清理,否则不但会发生内存泄漏,在线程池环境下线程复用还可能导致业务逻辑出现问题。
还没有评论,来说两句吧...