上一篇文章 Java內(nèi)存泄露學(xué)習(xí) ThreadLocal真的會(huì)內(nèi)存泄露嗎 提到ThreadLocal內(nèi)存泄露的問(wèn)題。我們也知道導(dǎo)致內(nèi)存泄露的一個(gè)關(guān)鍵點(diǎn)就是ThreadLocalMap.Entry的key是弱引用,如果gc回收key以后,value無(wú)法被訪問(wèn)也沒(méi)有回收就會(huì)內(nèi)存泄露。
那么jdk里面除了ThreadLocal還有其他地方有使用弱引用的嗎?它們是怎么解決內(nèi)存泄露呢?除了ThreadLocal的手動(dòng)remove,還有沒(méi)有其他好的辦法?
其實(shí)還有一個(gè)常見(jiàn)的類WeakHashMap,從名字上就可以看出它是HashMap的一個(gè)變種,翻閱源碼可以看見(jiàn)核心實(shí)現(xiàn)就是Entry使用了弱引用,并且配合了ReferenceQueue來(lái)很好地解決內(nèi)存泄露的問(wèn)題。
關(guān)于Java的四種引用的區(qū)別這里不做討論,針對(duì)弱引用等相關(guān)的ReferenceQueue我們來(lái)了解一下。
在WeakReference類的構(gòu)造方法中我們看到,可以傳入一個(gè)ReferenceQueue。它的作用是當(dāng)弱引用指向的referent被回收以后,這個(gè)弱引用會(huì)被添加到queue里面。
如果我們讀取這個(gè)queue,就可以做跟這個(gè)referent相關(guān)聯(lián)的對(duì)象的回收工作。這么說(shuō)可能不太好理解,我們用jdk自帶的WeakHashMap來(lái)學(xué)習(xí)一下。
/**
* Reference queue for cleared WeakEntries
*/
private final ReferenceQueue<Object> queue = new ReferenceQueue<>();
// 上面定義了一個(gè)queue,然后在下面Map的Entry里面繼承了WeakReference并調(diào)用父類構(gòu)造方法傳入queue,
// 所以當(dāng)key被回收的時(shí)候,Entry這個(gè)reference對(duì)象就會(huì)被添加到queue里面,讀取這個(gè)隊(duì)列就可以做清理工作
// 這是WeakHashMap的內(nèi)部類,實(shí)現(xiàn)了同HashMap.Entry類似的功能(hashmap1.8以后優(yōu)化采用了紅黑樹(shù),跟1.8之前的hashmap類似),
// 區(qū)別就是繼承了WeakReference使得內(nèi)存不足的時(shí)候條目可以被回收
private static class Entry<K,V> extends WeakReference<Object> implements Map.Entry<K,V> {
V value;
final int hash;
Entry<K,V> next;
/**
* Creates new entry.
*/
Entry(Object key, V value,
ReferenceQueue<Object> queue,
int hash, Entry<K,V> next) {
super(key, queue);
this.value = value;
this.hash = hash;
this.next = next;
}
我們簡(jiǎn)單圖解一下WeakHashMap的結(jié)構(gòu),Table[]數(shù)組+Entry鏈表的實(shí)現(xiàn)。由于Entry的key是弱引用,當(dāng)Entry1的key被gc回收以后,其實(shí)這個(gè)Entry已經(jīng)沒(méi)有存在的意義了,但是還是占著一個(gè)size,value也會(huì)造成內(nèi)存泄露。

那么怎么樣才能回收掉這個(gè)Entry呢,WeakHashMap有一個(gè)核心方法expungeStaleEntries(),我們用Entry1 Entry2來(lái)代入,看看這段代碼的邏輯
/**
* Expunges stale entries from the table.
*/
private void expungeStaleEntries() {
for (Object x; (x = queue.poll()) != null; ) { // 假設(shè)Entry1的key被gc 那么Entry1就會(huì)被放入queue,這里x返回Entry1 即需要被清理的entry
synchronized (queue) { // 同步操作 保證線程安全
@SuppressWarnings("unchecked")
Entry<K,V> e = (Entry<K,V>) x; // e -> Entry1
int i = indexFor(e.hash, table.length); // 找到index,假設(shè)就是0
Entry<K,V> prev = table[i]; // index0上目前存放的第一個(gè)entry,可能是Entry1也可能不是,因?yàn)镋ntry1可能存放在鏈表的下一個(gè)節(jié)點(diǎn),我們這個(gè)圖例中Entry1就是第一個(gè)
Entry<K,V> p = prev; // prev-> index0 p -> index0
while (p != null) { // 如果index0 存放的是空,那說(shuō)明被回收掉的那個(gè)Entry1肯定不在這里,不做清理操作
Entry<K,V> next = p.next; // 找到next 是Entry2
if (p == e) { // 如果被回收的Entry1 就是index0的第一個(gè)entry,那么進(jìn)入分支邏輯進(jìn)行回收操作
if (prev == e) // index0位置上的table[0] 直接指向 next,即Entry2
table[i] = next;
else
prev.next = next;
// Must not null out e.next;
// stale entries may be in use by a HashIterator
e.value = null; // Help GC value引用釋放掉幫助gc回收
size--; // Entry被回收了一個(gè),size要減1
break; // 做完這個(gè)entry的清理操作,繼續(xù)poll()進(jìn)行下一輪操作
}
prev = p; // 如果被回收的Entry1不是index0的第一個(gè)entry,那么繼續(xù)next往下找
p = next;
}
}
}
}
那么這個(gè)expungeStaleEntries方法是在何時(shí)被調(diào)用的呢,從調(diào)用層級(jí)圖中我們看到get(),put(),remove()等方法都有被調(diào)用到,也就是說(shuō)在正常使用api的時(shí)候就會(huì)默認(rèn)做entry的清理工作,防止內(nèi)存泄露。

看到這里,也許會(huì)想,既然有天然的ReferenceQueue支持,那么ThreadLocal為啥不采用這種方法來(lái)解決內(nèi)存泄露呢?具體原因就不知道了,個(gè)人看法是可以采用ReferenceQueue的,或許ThreadLocalMap的結(jié)構(gòu)比較簡(jiǎn)單,且ThreadLocal官方推薦的就是static用法,并提供了相應(yīng)的remove()方法來(lái)清理內(nèi)存,也是一種解決思路。
總結(jié)
- ThreadLocal依靠半自動(dòng)的無(wú)效entry清理+remove方法解決內(nèi)存泄露。
- WeakHashMap依靠jdk提供的ReferenceQueue來(lái)清理無(wú)效entry解決內(nèi)存泄露。
- ThreadLocal能否使用WeakHashMap的思路呢?個(gè)人認(rèn)為可以,不過(guò)jdk的實(shí)現(xiàn)差別我們不去揣摩。
- 如果平時(shí)業(yè)務(wù)代碼中有需要使用到弱引用軟引用等,可以參考WeakHashMap的思路來(lái)預(yù)防內(nèi)存泄露。