別再找了,一文徹底解析Java 中的弱引用(參考官網)

面試君發表於2019-09-27

概覽

本文會通過對弱引用的定義講起,然後通過案例的使用一步一步的深入原始碼進行分析其原理,從而讓讀者深刻的理解什麼是弱引用,如何使用弱引用,什麼場景下會使用弱引用,弱引用可以解決什麼樣的問題,以及它的原始碼實現是怎樣的,其中會涉及的記憶體溢位,垃圾回收原理

作用:

jdk 官網解釋:

弱引用主要應用在不阻止它的key或者value 被回收的mapping。直接貼英文吧,翻譯水平有限(weak references are for implementing canonicalizing mappings that do not prevent their keys (or values) from being reclaimed)

個人理解:

弱引用的出現就是為了垃圾回收服務的。它引用一個物件,但是並不阻止該物件被回收。如果使用一個強引用的話,只要該引用存在,那麼被引用的物件是不能被回收的。弱引用則沒有這個問題。在垃圾回收器執行的時候,如果一個物件的所有引用都是弱引用的話,該物件會被回收

案例深度解析:

理想的情況下,我們希望當我們不再使用一個物件的時候,能夠在gc 發生的時候就把它回收掉。但是有些時候,由於我們的粗忽,在壞的情況下會導致記憶體溢位。這種案例尤其發生在一個生命使用週期很長的map 存放了很多實際使用生命週期短的物件。請看下面這個例子


public class StrongRefenceDemo {


    static Map<String, String> map;

    public static void main(String[] args) throws Exception {

        StrongRefenceDemo demo = new StrongRefenceDemo();
        demo.strongTest();
        System.out.println("gc 發生前:" + map.size());
        System.out.println("開始通知GC");
        //注意,這裡只是通過垃圾回收器進行垃圾回收,並不一定馬上執行
        System.gc();
        Thread.sleep(1000 * 5);
        System.out.println("gc 發生後:" + map.size());

    }


    /**
     * 強引用測試
     */
    public void strongTest() {
        map = new HashMap<>();
        String key = new String("key");
        String value = new String("value");
        map.put(key, value);
        key = null;
        value = null;
    }

}

複製程式碼

執行後輸出結果:

gc 發生前:1
開始通知GC
gc 發生後:1

複製程式碼

從輸出的結果可以看到,即使我們通過把key和value 設定為null 來告訴jvm,我們不再使用這個物件了,map 裡面物件依然沒有被GC 回收,因為key和value 被一個強引用map 指向,根據可達性判斷,垃圾回收器是不能回收掉key和value 這個物件的。map 被定義為statis 的靜態變數,是一個使用生命週期很長的物件。在strongTest()方法中存在了一個key和value 的區域性變數,它隨著方法的執行完,這個變數的生命使用週期就結束了,但是粗糙的程式設計師忘記remove 了,這個時候垃圾回收器是不能回收它的。如果這種生命週期相對短的物件很多,最終就有可能消耗掉JVM中全部的記憶體。

但是這裡我有一個好奇,假如這裡的key和value 指向的物件在執行完strongTest()方法 以後用不著了,但是我可能又不是很好的判斷去主動呼叫remove 來移除它。想要垃圾回收器自己判斷回收掉可不可以呢?答案其實是可以的,這個時候就是弱引用上場了,請看下面程式


public class WeakRefenceDemo {

    static Map<WeakReference<String>, String> map;

    public static void main(String[] args) throws Exception {

        WeakRefenceDemo demo = new WeakRefenceDemo();
        demo.weakTest();
        System.out.println("gc 發生前:" + map.size());
        System.out.println("開始通知GC");
        //注意,這裡只是通過垃圾回收器進行垃圾回收,並不一定馬上執行
        System.gc();
        Thread.sleep(1000 * 5);
        System.out.println("gc 發生後:" + map.size());


    }


    /**
     * 若引用測試
     */
    public void weakTest() {
        map = new WeakHashMap<>();
        String key = new String("key");
        String value = new String("value");
        map.put(new WeakReference<>(key), value);
        key = null;
        value = null;
    }
}

複製程式碼

執行上面程式碼輸出結果

gc 發生前:1
開始通知GC
gc 發生後:0
複製程式碼

從輸出結果0,我們可以判斷已經成功被垃圾回收了。what?整個過程我們只是把HashMap 換成了WeakHashMap,並且key 由String 換成了WeakReference。其實就是由於字串只有弱引用指向,所以可以被垃圾回收掉。是不是很簡單,如果到這裡你就停止研究弱引用了,那就太暴殄天物了

WeakHashMap 深度解析

上面的程式片段中,其實只有key 設定了為弱引用new WeakReference<>(key),那正常也就只有這個key 對應的記憶體被回收而已,由於沒有呼叫remove ,裡面的value 和entry 也是不會回收掉的,那為什麼最後輸出的size 是0 呢? 很好的問題,我們深入去看WeakHashMap 的原始碼,我們發現了一個神奇的方法expungeStaleEntries()。在看原始碼之前先解析下引用佇列的概念: 在弱引用被回收的時候會把該物件放到引用佇列中,也就意味著從引用佇列中獲取的物件都是被回收的物件,先解釋到這裡,足以滿足我們下面的原始碼分析了,接下來會做詳細的解析


    /**
     * Expunges stale entries from the table.
     */
    private void expungeStaleEntries() {
    //這裡從引用佇列裡面取出一個已經被回收的物件
        for (Object x; (x = queue.poll()) != null; ) {
            synchronized (queue) {
                @SuppressWarnings("unchecked")
                    Entry<K,V> e = (Entry<K,V>) x;
                int i = indexFor(e.hash, table.length);

                Entry<K,V> prev = table[i];
                Entry<K,V> p = prev;
                //下面就是通過遍歷連結串列來設定值為null 來告訴垃圾回收器回收掉
                //注意WeakHashMap 和HashMap 的資料結構都是通過陣列+連結串列組成的,只有理解了這點才知道下面的程式碼做了什麼
                while (p != null) {
                    Entry<K,V> next = p.next;
                    //相等的時候,就意味著這個就是要回收的物件
                    if (p == e) {
                    //下面就是讓回收物件不再被引用
                        if (prev == e)
                   
                            table[i] = next;
                        else
                        
                            prev.next = next;
                        // Must not null out e.next;
                        // stale entries may be in use by a HashIterator
                        //這裡通過設定value 為null 來告訴垃圾回收
                        e.value = null; // Help GC
                        size--;
                        break;
                    }
                    prev = p;
                    p = next;
                }
            }
        }
    }
複製程式碼

從上面的程式碼片段,大概的意思就是從引用佇列裡面取出被回收的物件,然後和WeakHashMap 中的物件查詢,找到之後就把對應的value 也設定為null,並且把對應的entry 設定為null,來告訴GC 去回收它。從原始碼可以看到expungeStaleEntries() 這個方法在執行WeakHashMap中的任何方法的時候都會被呼叫到的

    /**
     * Returns the table after first expunging stale entries.
     */
    private Entry<K,V>[] getTable() {
    //被呼叫
        expungeStaleEntries();
        return table;
    }
    
        /**
     * Returns the number of key-value mappings in this map.
     * This result is a snapshot, and may not reflect unprocessed
     * entries that will be removed before next attempted access
     * because they are no longer referenced.
     */
    public int size() {
        if (size == 0)
            return 0;
            //被呼叫
        expungeStaleEntries();
        return size;
    }
複製程式碼

到這裡也就完全明白為什麼value 不設定為弱引用和沒有顯性的呼叫remove 方法也可以回收掉了

引用佇列

從上面的的原始碼中,我們大概知道了引用佇列的使用,那為什麼要使用引用佇列呢?假如沒有引用佇列,上面的例子我們就需要遍歷全部的元素一個一個的去找,如果數量少那還好,如果數量多的時候,肯定就會出現一些效能問題。有了引用佇列那就輕鬆可以解決上面的問題了。從WeakReference 原始碼中我們可以看到有兩個建構函式,第二個是需要傳入引用佇列的

    public WeakReference(T referent) {
       super(referent);
   }
   
   public WeakReference(T referent, ReferenceQueue<? super T> q) {
       super(referent, q);
   }
複製程式碼

引用佇列hello word

Object referent = new Object();
ReferenceQueue<Object> referenceQueue = new ReferenceQueue<>();

WeakReference weakReference1 = new WeakReference<>(referent);
WeakReference weakReference2 = new WeakReference<>(referent, referenceQueue);
複製程式碼

使用中需要注意的細節: 由於弱引用的物件在GC 發生的時候都可能會被回收掉,所以在使用之前我們都需要判斷下是否為null 來避免空指標異常

Object referent3 = weakReference2.get();
if (referent3 != null) {
    // GC hasn't removed the instance yet
} else {
    // GC has cleared the instance
}
複製程式碼

總結

  1. 弱引用的出現是為了垃圾回收的
  2. 一個物件只有弱引用指向它的時候,它是可以被回收的
  3. 弱引用是在GC 發生的時候就進行回收,不管當時記憶體是否充足
  4. 如果你在建立弱引用指定一個引用佇列的話,弱引用物件被回收的時候,會把該物件放入引用佇列中
  5. 為了安全使用,每次都要判斷下是否為空來判斷該物件是否已經被回收,來避免空指標異常

看完兩件事

如果你覺得這篇內容對你挺有啟發,我想邀請你幫我2個小忙:

  1. 點贊,讓更多的人也能看到這篇內容(收藏不點贊,都是耍流氓 -_-)
  2. 關注公眾號「面試bat」,不定期分享原創知識,原創不易,請多支援(裡面還提供刷題小程式哦)。

別再找了,一文徹底解析Java 中的弱引用(參考官網)

jdk8官方文件解析弱引用

相關文章