JDK1.8源碼逐字逐句帶你理解WeakHashMap底層

引言

WeakHashMap其實也是java不常見的東西,但是和linkedHashMap一樣,有它自己獨特的功能。在本篇博文中我會用例子詳細介紹它獨有的屬性,同時會對照源碼來解釋爲什麼它具備這樣的功能。在知識點中會擴展關於引用的相關知識,幫助後面的理解。筆者目前整理的一些blog針對面試都是超高頻出現的。大家可以點擊鏈接:http://blog.csdn.net/u012403290

技術點

1、java中的引用
關於java中的引用,其實我在“GC-談談“生死””這篇文章中就詳細介紹過引用的概念,從原來的粗獷定義到現在的定義(http://blog.csdn.net/u012403290/article/details/65698856)。引用類型主要分爲4種:①強引用;②軟引用;③弱引用;④虛引用。強引用就是永遠不會回收掉被引用的對象,比如說我們代碼中new出來的對象。軟引用表示有用但是非必需的,如果系統內存資源緊張,可能就會被回收;弱引用表示非必需的對象,只能存活到下一次垃圾回收發生之前;虛引用是最弱的,這個引用無法操作對象。在java中有與之對應的對象:SoftReference(軟引用), WeakReference(弱引用),PhantomReference(虛引用)。在我們今天要研究的WeakHashMap中用WeakReference來實現。

2、引用隊列ReferenceQueue
根據本人的理解,引用隊列就相當於一個電話簿一樣的東西,用於監聽和管理在引用對象中被回收的對象。具體我用一段代碼來解釋:

    Object o1 = new Object();
    Integer o2 = new Integer((int) o1);

比如說上面兩段代碼,在我們看來如果o2對象不被回收的話,o1永遠都不可能被回收。但是在引用(Reference)中,存在這麼一個情況:如果o1對象除了在o2中有引用之外沒有別的地方存在引用,那麼就可以回收o1。然後當這個o1被回收之後,我們就需要把o2放入引用隊列中,所以引用隊列(ReferenceQueue)就是Reference的監聽器。在WeakHashMap中就是通過ReferenceQueue來反向處理map中的數據,如果對象被回收了,那麼就需要把map中的對應數據移除。

一個例子

或許對於上面的介紹,很多人都看不懂的,接下來我先用HashMap建立一個例子幫大家理解:

package com.brickworkers;

import java.lang.ref.ReferenceQueue;
import java.lang.ref.WeakReference;
import java.util.HashMap;
import java.util.Map;

public class ReferenceTest {
    private static final int _1MB = 1024*1024;//設置大小爲1MB

    public static void main(String[] args) throws InterruptedException {
        ReferenceQueue<Object> referenceQueue = new ReferenceQueue<Object>();//用引用隊列進行監控引用的回收情況
        Object value = new Object();
        Map<Object, Object> map = new HashMap<Object, Object>();
        for (int i = 0; i < 100; i++) {//循環100次把數據插入到弱應用中(WeakReference), 同時把弱引用作爲key存入HashMap
            byte[] bytes = new byte[_1MB];
            //每個引用中都有關聯引用隊列(referenceQueue)的構造器,用引用隊列監聽回收情況
            //如此,那麼每次WeakReference中的bytes被回收之後,那麼這個weakReference對象就會放入引用隊列
            WeakReference<byte[]> weakReference = new WeakReference<byte[]>(bytes, referenceQueue);
            map.put(weakReference, value);
        }

        Thread thread = new Thread(new Runnable() {//線程通過調用引用隊列的情況查看那些對象被回收
            @SuppressWarnings("unchecked")
            public void run() {
                try {
                    int cnt = 0;
                    WeakReference<byte[]> k;
                    while ((k = (WeakReference<byte[]>) referenceQueue.remove()) != null) {//返回被回收對象的引用(注意本例中被回收的是bytes)
                        System.out.println((cnt++)+"回收了"+k);
                        System.out.println("map的size = " + map.size());//用於監控map的存儲數量有沒有發生變化
                    }
                } catch (Exception e) {
                    // TODO: handle exception
                }
            }
        });

        thread.start();
    }


}
    //截取一部分輸出:
//  53回收了java.lang.ref.WeakReference@232204a1
//  map的size = 100
//  54回收了java.lang.ref.WeakReference@355da254
//  map的size = 100
//  55回收了java.lang.ref.WeakReference@d716361
//  map的size = 100
//  56回收了java.lang.ref.WeakReference@74a14482
//  map的size = 100
//  57回收了java.lang.ref.WeakReference@12a3a380
//  map的size = 100
//  58回收了java.lang.ref.WeakReference@60e53b93
//  map的size = 100

注意,回收的對象是bytes,並不是weakReference, 這也是爲什麼HashMap中的數據長度並沒有發生變化的原因。我們再梳理一下運行流程:
1、bytes對象存入到weakReference對象中。
2、weakReference對象作爲key,一個Object作爲值存入HashMap中
2、GC回收了bytes對象,這個時候就要把引用這個對象的weakReference對象存儲到ReferenceQueue中
3、死循環ReferenceQueue, 打印出被回收的對象。

下面就是上來的引用關係:
HashMap ——>weakReference——>byte[],千萬注意被回收的是byte[]對象。
其實,上面這個邏輯就是核心WeakHashMap的實現,WeakHashMap只不過比上述的代碼多了一步:把引用回收的對象從Map中移除罷了。

WeakHashMap來實現上面例子

package com.brickworkers;

import java.util.Map;
import java.util.WeakHashMap;

public class ReferenceTest {
    private static final int _1MB = 1024*1024;//設置大小爲1MB

    public static void main(String[] args) throws InterruptedException {
        Object value = new Object();
        Map<Object, Object> map = new WeakHashMap<Object, Object>();
        for (int i = 0; i < 100; i++) {//循環100次把數據插入WeakHashMap中
            byte[] bytes = new byte[_1MB];
            map.put(bytes, value);
        }
        while (true) {//死循環監控map大小變化
            Thread.sleep(500);//稍稍停頓,效果更直觀
            System.out.println(map.size());//打印WeakHashMap的大小
            System.gc();//建議系統進行GC
        }
    }

    //截取一部分輸出:
//  41
//  0
//  0
//  0


}

以上的代碼就是用WeakHashMap來實現了,你會說爲什麼不直接在最上面的代碼把HashMap改成WeakHashMap就行了呢?不行的!WeakHashMap在類的內部就構建了引用隊列(ReferenceQueue)和弱引用(weakReference ),具體的下面源碼會介紹到。我們先來分析和解釋一下上面的代碼,一般人會有2個疑問:
①爲什麼我插入100個數據,第一次打印是41呢?
因爲在插入的過程中已經觸發過GC了,你可以把size的打印放到循環內部,你就會發現原因。同時爲什麼是到達41呢?這個和你的內存有關係,如果內存很富足,它就不會發生GC,而且弱引用是雞肋一般的東西:食之無味,棄之可惜。他們只能存活到下次GC之前。
②爲什麼後來又變成0了呢?
因爲在打印了大小之後,我建議系統(System.gc())發起一次GC操作,爲什麼說是建議呢?因爲系統不一定會接收到你 指令就會發生GC的。一旦GC發生,那麼弱引用就會被清除,導致WeakHashMap的大小爲0。
同時,值得一提的是,存在WeakHashMap中的數據,並不會平白無故就給你移除了map中的數據,必然是你觸發了一些操作,在上述代碼中size方法就會觸發這個操作,下面是size的源碼:

    /**
     * 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;
    }

當然不僅僅在size方法會觸發,下面源碼介紹我們會講到。

逐字逐句理解WeakHashMap

當然,理解Map相關的,需要你對Map有所瞭解,如果你不是很瞭解請參考一下我寫的關於HashMap的博文:http://blog.csdn.net/u012403290/article/details/65442646
1、在WeakHashMap中核心成員變量(關於HashMap中已存在的不再贅述):
①引用隊列

    /**
     * Reference queue for cleared WeakEntries
     */
    private final ReferenceQueue<Object> queue = new ReferenceQueue<>();

這個隊列其實就是我們前面研究過的,用於監控對象回收的情況。

②靜態內部類Entry K,V
下面是我截取的部分源碼

    /**
     * The entries in this hash table extend WeakReference, using its main ref
     * field as the key.
     */
     //繼承了弱引用WeakReference, 同時實現了Map.Entry接口
    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;
        }
        .
        .
        .//這裏還重寫了一些方法
        }

這個靜態內部類也是WeakHashMap的核心,因爲它把key值封裝進了弱引用(WeakReference)中,這樣一來,就回到了我們最前面的例子中,GC的時候可以回收掉弱引用對象中引用的對象(很拗口是不是?我自己寫的自己讀都拗口,其實就是真正的key值被弱引用WeakReference包裝了),在源碼中super實現了弱引用與引用隊列關聯的構造器,這樣引用隊列可以對弱引用進行監控了。

③核心移除map中K,V方法
如此一來,結合最前面的代碼,我們對WeakHashMap的理解已經基本成型了。接下來,我們要解釋一下爲什麼對象回收之後,map中的對應K,V也會被移除,核心就是下面這個方法:

 /**
     * Expunges stale entries from the table.
     */
    private void expungeStaleEntries() {
        for (Object x; (x = queue.poll()) != null; ) {//存在對象被GC, 那麼就需要移除map中對應的數據
            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;
                while (p != null) {//如果P節點存在
                    Entry<K,V> next = p.next;//定義一個next節點指向p的下個節點
                    if (p == e) {//如果P就是當前節點
                        if (prev == e)
                            table[i] = next;//意思就是桶中第一個數據就是需要移除的,直接把第二個節點放到頭節點的位置
                        else
                            prev.next = next;//那就把上個節點的下個節點指向p後面的節點
                        // Must not null out e.next;
                        // stale entries may be in use by a HashIterator
                        e.value = null; // Help GC幫助GC,直接刪除e的對應value值
                        size--;//減少WeakHashMap的大小
                        break;//結束
                    }
                    prev = p;
                    p = next;
                }
            }
        }
    }

或許有小夥伴看源代碼有些吃力,我把上面這段代碼主要做的事情寫出來:
①:循環遍歷引用隊列(queue), 如果發現某個對象被GC了,那麼就開始處理。
②:如果被處理的這個節點是頭節點,那麼直接把該節點的下個節點放到頭節點,然後幫助GC去除value的引用,接着把WeakHashMap的大小減1。
③:如果被處理的這個節點不是頭結點,那麼就需要把這個節點的上個節點中的next指針直接指向當前節點的下個節點。意思就是a->b->c,這個時候要移除b,那麼就變成a->c。然後幫助GC去除value的引用,接着把WeakHashMap的大小減1。

那麼在那些時候出發這個expungeStaleEntries方法呢?查詢源碼之後就會發現好多方法都會調用這個方法:

//size方法
    public int size() {
        if (size == 0)
            return 0;
        expungeStaleEntries();//去除被回收的對象
        return size;
    }

//getTable方法(這個方法是put和get方法的輔助方法)
    /**
     * Returns the table after first expunging stale entries.
     */
    private Entry<K,V>[] getTable() {
        expungeStaleEntries();//去除被回收的對象
        return table;
    }

//resize擴容方法
    void resize(int newCapacity) {
        Entry<K,V>[] oldTable = getTable();
        int oldCapacity = oldTable.length;
        if (oldCapacity == MAXIMUM_CAPACITY) {
            threshold = Integer.MAX_VALUE;
            return;
        }

        Entry<K,V>[] newTable = newTable(newCapacity);
        transfer(oldTable, newTable);
        table = newTable;

        /*
         * If ignoring null elements and processing ref queue caused massive
         * shrinkage, then restore old table.  This should be rare, but avoids
         * unbounded expansion of garbage-filled tables.
         */
        if (size >= threshold / 2) {
            threshold = (int)(newCapacity * loadFactor);
        } else {
            expungeStaleEntries();//去除被回收的對象
            transfer(newTable, oldTable);
            table = oldTable;
        }
    }

//get方法(基於上面說的getTable方法)
    public V get(Object key) {
        Object k = maskNull(key);
        int h = hash(k);
        Entry<K,V>[] tab = getTable();//getTable中包裝了expungeStaleEntries方法
        int index = indexFor(h, tab.length);
        Entry<K,V> e = tab[index];
        while (e != null) {
            if (e.hash == h && eq(k, e.get()))
                return e.value;
            e = e.next;
        }
        return null;
    }

//put方法(基於上面說的getTable方法)
    public V put(K key, V value) {
        Object k = maskNull(key);
        int h = hash(k);
        Entry<K,V>[] tab = getTable();//getTable中包裝了expungeStaleEntries方法
        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;
            }
        }

這個方法是滲透在很多方法裏面的,這裏就不繼續一一列舉了,同時關於WeakHashMap的添加(put),獲取(get), 擴容(resize)這裏就不一一介紹了,如果不清楚的,請去查看我寫的HashMap詳解,裏面我又詳細介紹過。

如果你覺得我那裏說的不對,或者有更好的解釋,歡迎留言交流。我說的並不一定對,可能只是一個簡單的指導作用,大家可以自己深入研究一下,或許會有別開新面的收穫哦。

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