從源碼淺析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進行了清理,降低了內存泄露的風險。