Java弱引用學(xué)習(xí) WeakHashMap、ReferenceQueue

上一篇文章 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)存泄露。

WeakHashMap.png

那么怎么樣才能回收掉這個(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)存泄露。


image.png

看到這里,也許會(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)存泄露。
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

友情鏈接更多精彩內(nèi)容