ThreadLocal内存泄漏分析
一、ThreadLocal结构
每个Thread都有一个ThreadLocalMap类型的成员变量threadLocals,ThreadLocalMap中有一个Entry[ ]类型的成员变量table,Entry保存对threadLocal对象的引用和对应set进去的值,Entry本身继承了WeakReference。
ThreadLocal更像一个工具,ThreadLocalMap实现了对内部Entry数组的操作,每一个Entry就像一个k/v键值对,保存着threadLocal引用和set进来的值。
//示例代码
ThreadLocal<Person> threadLocal=new ThreadLocal<>();
二、使用弱引用的原因
当threadLocal不再指向new ThreadLocal<>()这块内存时,如果Entry中有一个强引用指向这块内存,那么new ThreadLocal<>()这个对象就不会被回收,造成内存泄漏。
三、内存泄漏原因
当threadLocal对象被回收时,如果没有将之前set进去的对象移除,那么被set进去的对象还是会被Entry中的value引用,而且table也被线程中的ThreadLocalMap引用,导致之前set进去的对象不能被回收,造成内存泄漏。为了尽量避免这个问题,在每次对threadLocal进行set和get操作时,会检查用来存放Entry的table数组内是否有key为空的元素(也就是判断是否能通过Entry的get方法获取到弱引用所指向的threadLocal对象),如果存在这样的元素,那么会将Entry内的value置为null,并且将对应的table[i]置为null,这样在下次gc时就可以回收之前set进去的对象,并且也会清除table数组中无效的Entry。
private void set(ThreadLocal<?> key, Object value) {
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)]) {
ThreadLocal<?> k = e.get();
//如果之前已经set过了则覆盖之前的值
if (k == key) {
e.value = value;
return;
}
//如果当前位置的Entry指向的threadLocal对象已经被回收
//代替这个无效元素并进行一次对无效元素的扫描
if (k == null) {
replaceStaleEntry(key, value, i);
return;
}
}
tab[i] = new Entry(key, value);
int sz = ++size;
//先执行cleanSomeSlots尝试扫描数组中的无效元素并清除
//如果没有找到无效的元素那么判断table的size是否超过阈值,如果超过则扩容
if (!cleanSomeSlots(i, sz) && sz >= threshold)
rehash();
}
private boolean cleanSomeSlots(int i, int n) {
boolean removed = false;
Entry[] tab = table;
int len = tab.length;
do {
i = nextIndex(i, len);
Entry e = tab[i];
//如果Entry的弱引用指向的threadLocal对象已被回收,则此Entry为无效元素,需要被清除
if (e != null && e.get() == null) {
n = len;
removed = true;
i = expungeStaleEntry(i);
}
} while ( (n >>>= 1) != 0);
return removed;
}
private int expungeStaleEntry(int staleSlot) {
Entry[] tab = table;
int len = tab.length;
// expunge entry at staleSlot 清除指定位置的entry
tab[staleSlot].value = null;
tab[staleSlot] = null;
size--;
// Rehash until we encounter null
Entry e;
int i;
for (i = nextIndex(staleSlot, len);
(e = tab[i]) != null;
i = nextIndex(i, len)) {
ThreadLocal<?> k = e.get();
if (k == null) {
e.value = null;
tab[i] = null;
size--;
} else {
int h = k.threadLocalHashCode & (len - 1);
if (h != i) {
tab[i] = null;
while (tab[h] != null)
h = nextIndex(h, len);
tab[h] = e;
}
}
}
return i;
}
即使这样还是会造成内存泄漏。在调用set方法时,会从当前set进去的位置开始向后扫描无效元素并清除,如果在指定的几次扫描后没有扫描到失效元素的位置,那么失效的Entry及set进去的对象就会一直占用内存,而且如果不经常调用set或get方法则更容易出现内存泄漏。除非线程运行的时间很短,只有线程运行结束后相应的内存才会被释放。
由于ThreadLocal内部扫描无效元素的不确定性,我们在不使用threadLocal时应及时调用remove方法释放内存。