java源碼-WeakHashMap

開篇

?作為Map系列的最后一篇,我覺得有必要講講WeakHashMap這個(gè)類,因?yàn)檫@個(gè)類可以解決一些oom的問題,典型的場(chǎng)景是在一個(gè)HashMap中put不同的key/value對(duì)象腹鹉,如果此時(shí)設(shè)置key為null而未清除map當(dāng)中的key對(duì)象,那么就無法通過gc回收該對(duì)象。
?在這篇文章中我希望能夠講明白WeakHashMap是如何解決key和value的gc回收問題,希望能夠?qū)σ恍?yīng)用場(chǎng)景產(chǎn)生幫助。


WeakHashMap使用舉例

?在下面這個(gè)例子當(dāng)中通過設(shè)置w1=null,會(huì)觸發(fā)gc對(duì)WeakHashMap當(dāng)中的w1進(jìn)行主動(dòng)回收。

import java.util.Iterator;
import java.util.Map;
import java.util.WeakHashMap;
import java.util.Date;
import java.lang.ref.WeakReference;

public class WeakHashMapTest {

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

    private static void testWeakHashMapAPIs() {
        // 初始化3個(gè)“弱鍵”
        String w1 = new String("one");
        String w2 = new String("two");
        String w3 = new String("three");
        // 新建WeakHashMap
        Map wmap = new WeakHashMap();

        // 添加鍵值對(duì)
        wmap.put(w1, "w1");
        wmap.put(w2, "w2");
        wmap.put(w3, "w3");

        // 打印出wmap
        System.out.printf("\nwmap:%s\n",wmap );

        // containsKey(Object key) :是否包含鍵key
        System.out.printf("contains key two : %s\n",wmap.containsKey("two"));
        System.out.printf("contains key five : %s\n",wmap.containsKey("five"));

        // containsValue(Object value) :是否包含值value
        System.out.printf("contains value 0 : %s\n",wmap.containsValue(new Integer(0)));

        // remove(Object key) : 刪除鍵key對(duì)應(yīng)的鍵值對(duì)
        wmap.remove("three");

        System.out.printf("wmap: %s\n",wmap );



        // ---- 測(cè)試 WeakHashMap 的自動(dòng)回收特性 ----
    
        // 將w1設(shè)置null骤肛。
        // 這意味著“弱鍵”w1再?zèng)]有被其它對(duì)象引用腋颠,調(diào)用gc時(shí)會(huì)回收WeakHashMap中與“w1”對(duì)應(yīng)的鍵值對(duì)
        w1 = null;
        // 內(nèi)存回收繁成。這里,會(huì)回收WeakHashMap中與“w1”對(duì)應(yīng)的鍵值對(duì)
        System.gc();

        // 遍歷WeakHashMap
        Iterator iter = wmap.entrySet().iterator();
        while (iter.hasNext()) {
            Map.Entry en = (Map.Entry)iter.next();
            System.out.printf("next : %s - %s\n",en.getKey(),en.getValue());
        }
        // 打印WeakHashMap的實(shí)際大小
        System.out.printf(" after gc WeakHashMap size:%s\n", wmap.size());
    }
}

--------------------------------輸出結(jié)果--------------------------------
wmap:{three=w3, one=w1, two=w2}
contains key two : true
contains key five : false
contains value 0 : false
wmap: {one=w1, two=w2}
next : two - w2
 after gc WeakHashMap size:1


WeakHashMap類圖

?WeakHashMap的類圖非常簡(jiǎn)單淑玫,簡(jiǎn)單跟HashMap很像巾腕,基本上實(shí)現(xiàn)了Map接口而已,所以可以按照HashMap的角度進(jìn)行解讀混移。


WeakHashMap類圖


WeakHashMap的工作原理

?WeakHashMap的核心在于key通過WeakReference進(jìn)行包裝祠墅,弱引用(Weak Reference)簡(jiǎn)單來說就是將對(duì)象留在內(nèi)存的能力不是那么強(qiáng)的引用。使用WeakReference歌径,垃圾回收器會(huì)幫你來決定引用的對(duì)象何時(shí)回收并且將對(duì)象從內(nèi)存移除毁嗦。

?創(chuàng)建弱引用如下: WeakReference<Widget> weakWidget = new WeakReference<Widget>(widget); 使用weakWidget.get()就可以得到真實(shí)的Widget對(duì)象,因?yàn)槿跻貌荒茏钃趵厥掌鲗?duì)其回收回铛,你會(huì)發(fā)現(xiàn)(當(dāng)沒有任何強(qiáng)引用到widget對(duì)象時(shí))使用get時(shí)突然返回null狗准。

?解決上述的widget序列數(shù)記錄的問題克锣,最簡(jiǎn)單的辦法就是使用Java內(nèi)置的WeakHashMap類。WeakHashMap和HashMap幾乎一樣腔长,唯一的區(qū)別就是它的鍵(不是值!!!)使用WeakReference引用袭祟。當(dāng)WeakHashMap的鍵標(biāo)記為垃圾的時(shí)候,這個(gè)鍵對(duì)應(yīng)的條目就會(huì)自動(dòng)被移除捞附。這就避免了上面不需要的Widget對(duì)象手動(dòng)刪除的問題巾乳。使用WeakHashMap可以很便捷地轉(zhuǎn)為HashMap或者M(jìn)ap。

?引用隊(duì)列(Reference Queue)鸟召,一旦弱引用對(duì)象開始返回null胆绊,該弱引用指向的對(duì)象就被標(biāo)記成了垃圾。而這個(gè)弱引用對(duì)象(非其指向的對(duì)象)就沒有什么用了欧募。通常這時(shí)候需要進(jìn)行一些清理工作压状。比如WeakHashMap會(huì)在這時(shí)候移除沒用的條目來避免保存無限制增長的沒有意義的弱引用。

?引用隊(duì)列可以很容易地實(shí)現(xiàn)跟蹤不需要的引用跟继。當(dāng)你在構(gòu)造WeakReference時(shí)傳入一個(gè)ReferenceQueue對(duì)象种冬,當(dāng)該引用指向的對(duì)象被標(biāo)記為垃圾的時(shí)候,這個(gè)引用對(duì)象會(huì)自動(dòng)地加入到引用隊(duì)列里面舔糖。接下來娱两,你就可以在固定的周期,處理傳入的引用隊(duì)列剩盒,比如做一些清理工作來處理這些沒有用的引用對(duì)象谷婆。


WeakHashMap類定義

? WeakHashMap當(dāng)中對(duì)于保存key/value的數(shù)據(jù)結(jié)構(gòu)其實(shí)和HashMap是一致的,真正差別的在于Entry<K,V>變量的不同辽聊。
? Entry是繼承自WeakReference類,其構(gòu)造函數(shù)內(nèi)部通過super(key, queue)方法重新構(gòu)建了key的對(duì)象期贫,這個(gè)作用包括key能夠被gc回收跟匆,同時(shí)value也在具體的map的操作類似size()/put()等方法中被回收。
? WeakReference的用法可以參見下面的章節(jié)或者參考文章通砍,弄懂了WeakReference也就明白了WeakHashMap玛臂。

public class WeakHashMap<K,V>
    extends AbstractMap<K,V>
    implements Map<K,V> {

    private static final int DEFAULT_INITIAL_CAPACITY = 16;
    private static final int MAXIMUM_CAPACITY = 1 << 30;
    private static final float DEFAULT_LOAD_FACTOR = 0.75f;
    //存儲(chǔ)map的key/value的數(shù)據(jù)結(jié)構(gòu)
    Entry<K,V>[] table;
    private int size;
    private int threshold;
    private final float loadFactor;
    //用于回收map當(dāng)中value數(shù)據(jù)結(jié)構(gòu)的核心queue變量
    private final ReferenceQueue<Object> queue = new ReferenceQueue<>();

    private static class Entry<K,V> extends WeakReference<Object> implements Map.Entry<K,V> {
        V value;
        final int hash;
        Entry<K,V> next;

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

    public V put(K key, V value) {
        Object k = maskNull(key);
        int h = hash(k);
        Entry<K,V>[] tab = getTable();
        int i = indexFor(h, tab.length);

       //省略相同代碼的判斷

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


WeakHashMap的Entry初始化過程

? WeakHashMap本身的構(gòu)建函數(shù)其實(shí)跟HashMap是一致的,所以沒有需要說明的封孙,核心的Entry才是WeakHashMap的核心實(shí)現(xiàn)迹冤。
? Entry的初始化過程其實(shí)逐步通過super()調(diào)用到WeakReference進(jìn)行初始化的,就是說真正放到table當(dāng)中Entry的key繼承了WeakReference從而實(shí)現(xiàn)了WeakHashMapkey的可回收虎忌,而value的回收是通過ReferenceQueue<Object> queue來實(shí)現(xiàn)的泡徙。

private static class Entry<K,V> extends WeakReference<Object> implements Map.Entry<K,V> {
        V value;
        final int hash;
        Entry<K,V> next;

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



  public class WeakReference<T> extends Reference<T> {
    public WeakReference(T referent, ReferenceQueue<? super T> q) {
        super(referent, q);
    }
}



  public abstract class Reference<T> {
    Reference(T referent, ReferenceQueue<? super T> queue) {
        this.referent = referent;
        this.queue = (queue == null) ? ReferenceQueue.NULL : queue;
    }

}


WeakHashMap常用操作

WeakHashMap的回收機(jī)制

?WeakHashMap的Key使用WeakReference進(jìn)行包裝,垃圾回收器會(huì)幫你來決定引用的對(duì)象何時(shí)回收并且將對(duì)象從內(nèi)存移除膜蠢。
?構(gòu)造WeakReference的時(shí)候我們傳入了ReferenceQueue對(duì)象super(key, queue)堪藐,當(dāng)該引用指向的對(duì)象被標(biāo)記為垃圾的時(shí)候莉兰,這個(gè)引用對(duì)象會(huì)自動(dòng)地加入到引用隊(duì)列里面。在WeakHashMap的源碼當(dāng)中執(zhí)行這個(gè)操作的函數(shù)是expungeStaleEntries礁竞,而且是在WeakHashMap的get/set/size這種操作中嵌入了expungeStaleEntries操作糖荒。刪除的邏輯就是比較Entry對(duì)象是否相等。


WeakHashMap的put操作

?之所以分析put操作是因?yàn)樵趐ut操作過程中我們可以清晰地看到Entry的創(chuàng)建過程模捂,可以看到通過WeakReference 和ReferenceQueue來對(duì)key進(jìn)行包裝捶朵,從而保證了key在對(duì)象被設(shè)置為null的時(shí)候可以被自動(dòng)清除。
?在put的過程中可以看到resize()的過程狂男,我們以2倍的長度進(jìn)行resize泉孩,resize的內(nèi)部操作就是將數(shù)據(jù)從oldTable拷貝到newTable的過程,中間有一段反向的邏輯應(yīng)該是為了節(jié)省空間猜測(cè)并淋。

public V put(K key, V value) {
        Object k = maskNull(key);
        int h = hash(k);
        Entry<K,V>[] 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, value, queue, h, e);
        if (++size >= threshold)
            resize(tab.length * 2);
        return null;
    }


private static class Entry<K,V> extends WeakReference<Object> implements Map.Entry<K,V> {
        V value;
        final int hash;
        Entry<K,V> next;

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


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 (size >= threshold / 2) {
            threshold = (int)(newCapacity * loadFactor);
        } else {
            expungeStaleEntries();
            transfer(newTable, oldTable);
            table = oldTable;
        }
    }


WeakHashMap的get操作

? WeakHashMap的get操作當(dāng)中也是按照對(duì)key進(jìn)行hash后定位table桶寓搬,然后對(duì)table進(jìn)行一輪value的回收,然后在遍歷table桶下面所有的元素進(jìn)行比較返回查找值县耽。

public V get(Object key) {
        Object k = maskNull(key);
        int h = hash(k);
        Entry<K,V>[] tab = getTable();
        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;
    }

private Entry<K,V>[] getTable() {
        expungeStaleEntries();
        return 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;
                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
                        e.value = null; // Help GC
                        size--;
                        break;
                    }
                    prev = p;
                    p = next;
                }
            }
        }
    }


WeakHashMap迭代器

? WeakHashMap迭代器的實(shí)現(xiàn)是非常巧妙的句喷,通過只創(chuàng)建一個(gè)KeySet對(duì)象來完成迭代器的工作,從這點(diǎn)上我們也可以看出來WeakHashMap是線程不安全的兔毙。
? 類KeySet內(nèi)部通過創(chuàng)建KeyIterator類對(duì)象唾琼,KeyIterator繼承自類HashIterator,內(nèi)部的hasNext從table的尾部開始往前進(jìn)行遍歷澎剥,每次記錄上一次遍歷的位置從而繼續(xù)往前遍歷锡溯。hasNext判斷當(dāng)前的Entry是否為null,如果為null就繼續(xù)往前遍歷哑姚。
? KeyIterator的next方法負(fù)責(zé)返回當(dāng)前Entry的key值并將Entry往下一個(gè)Entry進(jìn)行推進(jìn)祭饭,所以next的分工是往下一個(gè)Entry推進(jìn),hasNext是table桶整體往前推進(jìn)叙量。

public Set<K> keySet() {
        Set<K> ks = keySet;
        if (ks == null) {
            ks = new KeySet();
            keySet = ks;
        }
        return ks;
    }

    private class KeySet extends AbstractSet<K> {
        public Iterator<K> iterator() {
            return new KeyIterator();
        }
    }


private class KeyIterator extends HashIterator<K> {
        public K next() {
            return nextEntry().getKey();
        }
    }


private abstract class HashIterator<T> implements Iterator<T> {
        private int index;
        private Entry<K,V> entry;
        private Entry<K,V> lastReturned;
        private int expectedModCount = modCount;
        private Object nextKey;
        private Object currentKey;

        HashIterator() {
            index = isEmpty() ? 0 : table.length;
        }

        public boolean hasNext() {
            Entry<K,V>[] t = table;

            while (nextKey == null) {
                Entry<K,V> e = entry;
                int i = index;
                while (e == null && i > 0)
                    e = t[--i];
                entry = e;
                // index從上一個(gè)位置開始找起
                index = i;
                if (e == null) {
                    currentKey = null;
                    return false;
                }
                nextKey = e.get(); 
                if (nextKey == null)
                    entry = entry.next;
            }
            return true;
        }

        protected Entry<K,V> nextEntry() {
            if (modCount != expectedModCount)
                throw new ConcurrentModificationException();
            if (nextKey == null && !hasNext())
                throw new NoSuchElementException();

            lastReturned = entry;
            entry = entry.next;
            currentKey = nextKey;
            nextKey = null;
            return lastReturned;
        }

        public void remove() {
            if (lastReturned == null)
                throw new IllegalStateException();
            if (modCount != expectedModCount)
                throw new ConcurrentModificationException();

            WeakHashMap.this.remove(currentKey);
            expectedModCount = modCount;
            lastReturned = null;
            currentKey = null;
        }
    }


參考文章

理解Java中的弱引用
Java 集合系列13之 WeakHashMap詳細(xì)介紹(源碼解析)和使用示例

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末倡蝙,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子绞佩,更是在濱河造成了極大的恐慌寺鸥,老刑警劉巖,帶你破解...
    沈念sama閱讀 206,723評(píng)論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件品山,死亡現(xiàn)場(chǎng)離奇詭異胆建,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)肘交,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,485評(píng)論 2 382
  • 文/潘曉璐 我一進(jìn)店門笆载,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事宰译¢苎粒” “怎么了?”我有些...
    開封第一講書人閱讀 152,998評(píng)論 0 344
  • 文/不壞的土叔 我叫張陵沿侈,是天一觀的道長闯第。 經(jīng)常有香客問我,道長缀拭,這世上最難降的妖魔是什么咳短? 我笑而不...
    開封第一講書人閱讀 55,323評(píng)論 1 279
  • 正文 為了忘掉前任,我火速辦了婚禮蛛淋,結(jié)果婚禮上咙好,老公的妹妹穿的比我還像新娘。我一直安慰自己褐荷,他們只是感情好勾效,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,355評(píng)論 5 374
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著叛甫,像睡著了一般层宫。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上其监,一...
    開封第一講書人閱讀 49,079評(píng)論 1 285
  • 那天萌腿,我揣著相機(jī)與錄音,去河邊找鬼抖苦。 笑死毁菱,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的锌历。 我是一名探鬼主播贮庞,決...
    沈念sama閱讀 38,389評(píng)論 3 400
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼辩涝!你這毒婦竟也來了贸伐?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 37,019評(píng)論 0 259
  • 序言:老撾萬榮一對(duì)情侶失蹤怔揩,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后脯丝,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體商膊,經(jīng)...
    沈念sama閱讀 43,519評(píng)論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 35,971評(píng)論 2 325
  • 正文 我和宋清朗相戀三年宠进,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了晕拆。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 38,100評(píng)論 1 333
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖实幕,靈堂內(nèi)的尸體忽然破棺而出吝镣,到底是詐尸還是另有隱情,我是刑警寧澤昆庇,帶...
    沈念sama閱讀 33,738評(píng)論 4 324
  • 正文 年R本政府宣布末贾,位于F島的核電站,受9級(jí)特大地震影響整吆,放射性物質(zhì)發(fā)生泄漏拱撵。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,293評(píng)論 3 307
  • 文/蒙蒙 一表蝙、第九天 我趴在偏房一處隱蔽的房頂上張望拴测。 院中可真熱鬧,春花似錦府蛇、人聲如沸集索。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,289評(píng)論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽务荆。三九已至,卻和暖如春扰法,著一層夾襖步出監(jiān)牢的瞬間蛹含,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 31,517評(píng)論 1 262
  • 我被黑心中介騙來泰國打工塞颁, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留浦箱,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 45,547評(píng)論 2 354
  • 正文 我出身青樓祠锣,卻偏偏與公主長得像酷窥,于是被迫代替她去往敵國和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子伴网,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,834評(píng)論 2 345

推薦閱讀更多精彩內(nèi)容