从源码浅析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);
}
ThreadLocalMap getMap(Thread t) {
return t.threadLocals;
}
每个Thread对象有个名为threadLocals的Map,key是ThreadLocal对象,值是要存储的Value。这个名为threadLocals的Map的实现是ThreadLocalMap,它是ThreadLocal的内部类。这样子,当使用完ThradLocal之后,如果线程不去remove就会引发内存泄露问题。
Map中的Entry的Key是弱引用到ThreadLocal对象,当ThreadLocal外部的强引用置为Null后,Key会被GC掉,而value还存在着GCroot可达,链路如下:Thread->Thread.threadlocalMaps->entry->valueRef。所以value值无法被回收,存在内存泄露问题。
源码中已有的防内存泄露的改进
ThreadLocalMap中的set方法
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();
if (k == key) {
e.value = value;
return;
}
if (k == null) {
replaceStaleEntry(key, value, i);
return;
}
}
tab[i] = new Entry(key, value);
int sz = ++size;
if (!cleanSomeSlots(i, sz) && sz >= threshold)
rehash();
}
如果当前table[i]!=null的话说明hash冲突就需要向后环形查找,若在查找过程中遇到脏entry就通过replaceStaleEntry进行处理;
如果当前table[i]==null的话说明新的entry可以直接插入,但是插入后会调用cleanSomeSlots方法检测并清除脏entry;
插入值之后,再看看cleanSomeSlots方法,会去清理内存泄露的entry
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];
if (e != null && e.get() == null) {
n = len;
removed = true;
i = expungeStaleEntry(i);
}
} while ( (n >>>= 1) != 0);
return removed;
}
找到i之后,往后遍历,找出key为null的entry,并且清理掉。n为了控制扫描范围的,一开始n是entry的个数,如果发现了内存泄露的entry,则n变为哈希数组的长度,相当于增大了扫描范围。nextIndex是环形递增i。
再看看是如何清理的内存泄露的entry
private int expungeStaleEntry(int staleSlot) {
Entry[] tab = table;
int len = tab.length;
// expunge entry at staleSlot
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;
// Unlike Knuth 6.4 Algorithm R, we must scan until
// null because multiple entries could have been stale.
while (tab[h] != null)
h = nextIndex(h, len);
tab[h] = e;
}
}
}
return i;
}
这个方法将tab[staleSlot]及value置为null,使其可以被回收。接着,这个方法会继续从staleSlot开始进行环形遍历,直至遇到第一个下标i对应的entry是null则返回。当是非null时:如果key是null,说明是内存泄露的entry,则进行置null,使其可以被内存回收;如果是哈希桶位置不对,则进行纠正。
为什么要使用弱引用?
如果是强引用,如果没有进行remove则肯定会内存泄露,而弱引用,用于ThreadLocal进行了清理,降低了内存泄露的风险。