Java的弱引用—WeakHashMap

在《Effective Java》中的p23頁有涉及到WeakHashMap的相關知識,在這篇文章中做一個總結以及介紹一下相關知識。

在這裏我們分成三個部分來說明一下,這只是我自己參看JDK源碼和上網搜索資料得到的結果,如有錯誤,歡迎指出,我不勝榮幸。


WeakHashMap和HashMap有什麼不同

我們知道WeakHashMap是弱引用,而HashMap是強引用。
這就是說當我們給Java虛擬機分配的內存不足的時候,HashMap寧可拋出OutOfMemoryError異常也不會回收其相應的沒有被引用的對象,而我們的WeakHashMap則會回收存儲在其中但有被引用的對象。

如下的程序實例,運用的JVM參數爲:-Xmx5m -Xms5m -XX:+PrintGC

HashMap的測試代碼

    public static void main(String[] args) {

        HashMap hashMap = new HashMap();
        for (int i = 0; ; i++) {
            hashMap.put(i, new String("HashMap"));//一直往裏面加數據

            if (i % 1000 == 0) { //每隔一千次判斷一下有沒有對象被回收
                for (int j = 0; j < i; j++) {//遍歷一遍
                    if (hashMap.get(j) == null) {
                        System.out.println("第" + j + "個對象開始回收");
                        return;
                    }
                }
            }
        }
    }

我們運行程序,發現沒有任何對象被回收,最後拋出了異常

Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
    at java.util.HashMap.addEntry(HashMap.java:753)
    at java.util.HashMap.put(HashMap.java:385)

那我們再來證明一下WeakHashMap會回收其中存儲的沒有被引用的對象

    public static void main(String[] args) {

        WeakHashMap hashMap = new WeakHashMap();
        for (int i = 0; ; i++) {
            hashMap.put(i, new String("WeakHashMap"));//一直往裏面加數據

            if (i % 1000 == 0) { //每隔一千次判斷一下有沒有對象被回收
                for (int j = 0; j < i; j++) {//遍歷一遍
                    if (hashMap.get(j) == null) {
                        System.out.println("第" + j + "個對象開始回收");
                        return;
                    }
                }
            }
        }
    }

最後程序的輸出如下

[GC 1408K->787K(4992K), 0.0067048 secs]
第241個對象開始回收

可知WeakHashMap將其中存儲的鍵爲241的對象開始回收了。


我們接下來思考,WeakHashMap是怎樣知道要回收對象的呢?

WeakHashMap通過將一些沒有被引用的鍵的值賦值爲null,這樣的話就會告知GC去回收這些存儲的值了。

那麼也就是說,如果我們將其所有的鍵都添加引用,那麼其就不會被回收了?我們來寫一段代碼測試一下

    public static void main(String[] args) {

        WeakHashMap weakHashMap = new WeakHashMap();
        HashMap hashMap=new HashMap();
        for (int i = 0; ; i++) {

            Integer num=new Integer(i);
            hashMap.put(i,num);     // num 被引用

            weakHashMap.put(num, new String("WeakHashMap"));//將num改爲i就會有對象被回收

            if (i % 1000 == 0) { //每隔一千次判斷一下有沒有對象被回收
                for (int j = 0; j < i; j++) {//遍歷一遍
                    if (weakHashMap.get(j) == null) {
                        System.out.println("第" + j + "個對象開始回收");
                        return;
                    }
                }
            }
        }
    }

我們在HashMap中存儲了WeakHashMap的鍵,這樣就會是的其不會被回收,最後測試結果表示沒有對象被回收,程序也就像我們期待的一下報了OutOfMemoryError

當然可能會有人說,爲什麼這個OutOfMemoryError不是我們的HashMap引起的呢,然後WeakHashMap還沒有達到回收的條件?

針對這一點,我根據程序中的註釋將WeakHahMap的鍵從num改爲i,最後會發現有對象被回收。


既然WeakHashMap是將我們的鍵值設置爲null從而引起GC的,那麼我們將null作爲鍵存進去,爲什麼不會導致被回收呢?

這時候我們就需要看看其JDK的有關put方法的源碼了

    public V put(K key, V value) {
        K k = (K) maskNull(key);// 重點看這裏
        int h = HashMap.hash(k.hashCode());
        Entry[] tab = getTable();
        int i = indexFor(h, tab.length);

        for (Entry<K,V> e = tab[i]; e != null; e = e.next) {
            if (h == e.hash && eq(k, e.get())) {
                V oldValue = e.value;
                if (value != oldValue)
                    e.value = value;
                return oldValue;
            }
        }

        modCount++;
    Entry<K,V> e = tab[i];
        tab[i] = new Entry<K,V>(k, value, queue, h, e);
        if (++size >= threshold)
            resize(tab.length * 2);
        return null;
    }

我們重點看一下開始的判斷鍵時候爲nullmaskNull()方法。

    private static Object maskNull(Object key) {
        return (key == null ? NULL_KEY : key);
    }

我們發現,如果keynull的話,返回的是NULL_KEY這個靜態值,我們再來看一下這個值是什麼的時候,就恍然大悟了

    /**
     * Value representing null keys inside tables.
     */
    private static final Object NULL_KEY = new Object();

WeakHashMap在存儲null爲鍵的時候,其實存儲的是其本身的靜態成員變量Object,也就是說存儲的不是null

這也就解釋了爲什麼存儲鍵爲null不會被馬上回收。


最後我們來看一下程序是在什麼時候來判斷要將沒有引用的key標記爲null的呢?

這時候WeakHashMap中的一個非常重要的方法expungeStaleEntries()就登場了。

WeakHashMapput()get()remove()等等方法中都調用了一個getTable()方法,而這個getTable()方法的源碼如下:

    private Entry[] getTable() {
        expungeStaleEntries();
        return table;
    }

可以知道他們其實調用的都是expungeStaleEntries()方法。
可知這個方法是一個非常重要的方法。

[注].出自: Java集合框架:WeakHashMap,一篇非常優秀的博客!

首先我們看一下其實現的源碼

    /**
     * Expunges stale entries from the table.
     */
    private void expungeStaleEntries() {
    Entry<K,V> e;
        while ( (e = (Entry<K,V>) queue.poll()) != null) {
            int h = e.hash;
            int i = indexFor(h, table.length);

            Entry<K,V> prev = table[i];
            Entry<K,V> p = prev;
            while (p != null) {
                Entry<K,V> next = p.next;
                if (p == e) {
                    if (prev == e)
                        table[i] = next;
                    else
                        prev.next = next;
                    e.next = null;  // Help GC
                    e.value = null; //  "   "
                    size--;
                    break;
                }
                prev = p;
                p = next;
            }
        }
    }

可以看到每調用一次expungeStaleEntries()方法,就會在引用隊列中尋找是否有將要被清除的key對象,如果有則在table中找到其值,並將value設置爲nullnext指針也設置爲null,讓GC去回收這些資源。


2017-11-18 15:23 於上海

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章