天天看點

深入淺出的分析 WeakHashMap

在Map家族中,WeakHashMap 是一個很特殊的成員,從名字上看與 HashMap 相關,但是與 HashMap 有着很大的差别,翻譯成中文後表示弱 HashMap,俗稱緩存 HashMap。

作者:炸雞可樂

原文出處:www.pzblog.cn

一、摘要

在集合系列的第一章,咱們了解到,Map 的實作類有 HashMap、LinkedHashMap、TreeMap、IdentityHashMap、WeakHashMap、Hashtable、Properties 等等。

深入淺出的分析 WeakHashMap

本文主要從資料結構和算法層面,探讨 WeakHashMap 的實作。

二、簡介

剛剛咱們也介紹了,在 Map 家族中,WeakHashMap 是一個很特殊的成員,它的特殊之處在于 WeakHashMap 裡的元素可能會被 GC 自動删除,即使程式員沒有顯示調用 remove() 或者 clear() 方法。

換言之,當向 WeakHashMap 中添加元素的時候,再次周遊擷取元素,可能發現它已經不見了,我們來看看下面這個例子。

public static void main(String[] args) {
        Map weakHashMap = new WeakHashMap();
		
        //向weakHashMap中添加4個元素
        for (int i = 0; i < 3; i++) {
            weakHashMap.put("key-"+i, "value-"+ i);
        }
        //輸出添加的元素
        System.out.println("數組長度:"+weakHashMap.size() + ",輸出結果:" + weakHashMap);
		
		//主動觸發一次GC
        System.gc();
		
        //再輸出添加的元素
        System.out.println("數組長度:"+weakHashMap.size() + ",輸出結果:" + weakHashMap);
    }
           

輸出結果:

數組長度:3,輸出結果:{key-2=value-2, key-1=value-1, key-0=value-0}
數組長度:3,輸出結果:{}
           

當主動調用 GC 回收器的時候,再次查詢 WeakHashMap 裡面的資料的時候,内容為空。

更直覺的說,當使用 WeakHashMap 時,即使沒有顯式的添加或删除任何元素,也可能發生如下情況:

  • 調用兩次 size() 方法傳回不同的值;
  • 兩次調用 isEmpty() 方法,第一次傳回 false,第二次傳回 true;
  • 兩次調用 containsKey() 方法,第一次傳回 true,第二次傳回 false,盡管兩次使用的是同一個key;
  • 兩次調用 get() 方法,第一次傳回一個 value,第二次傳回 null,盡管兩次使用的是同一個對象。

要明白 WeekHashMap 的工作原理,還需要引入一個概念:弱引用。

我們都知道 Java 中記憶體是通過 GC 自動管理的,GC 會在程式運作過程中自動判斷哪些對象是可以被回收的,并在合适的時機進行記憶體釋放。

GC 判斷某個對象是否可被回收的依據是,是否有有效的引用指向該對象。如果沒有有效引用指向該對象(基本意味着不存在通路該對象的方式),那麼該對象就是可回收的。

2.1、對象引用介紹

從 JDK1.2 版本開始,把對象的引用分為四種級别,進而使程式更加靈活的控制對象的生命周期。這四種級别由高到低依次為:強引用、軟引用、弱引用和虛引用。

用表格整理之後,各個引用類型的差別如下:

深入淺出的分析 WeakHashMap
2.1.1、強引用

強引用是使用最普遍的引用,例如,我們建立一個對象:

//強引用類型
Object object=new Object();
           

如果一個對象具有強引用,那垃圾回收器絕不會回收它。當記憶體空間不足, Java 虛拟機甯願抛出 OutOfMemoryError 錯誤,使程式異常終止,也不會靠随意回收具有強引用的對象來解決記憶體不足的問題。

如果不使用時,要手動通過如下方式來弱化引用,如下:

//将對象設定為null,幫助垃圾收集器回收此對象
object=null;
           

這個時候,GC 認為該對象不存在引用,就可以回收這個對象,具體什麼時候收集這要取決于 GC 的算法。

2.1.2、軟引用

SoftReference

指向的對象,屬于軟引用,如下:

String str=new String("abc");

//軟引用
SoftReference<String> softRef=new SoftReference<String>(str);
           

如果一個對象隻具有軟引用,則記憶體空間足夠,垃圾回收器就不會回收它;如果記憶體空間不足了,就會進入垃圾回收器,Java 虛拟機就會把這個軟引用加入到與之關聯的

引用隊列

中,GC 進行回收處理。隻要垃圾回收器沒有回收它,該對象就可以被程式使用。

當記憶體不足時,等價于:

If(JVM.記憶體不足()) {
   str = null;  // 轉換為軟引用
   System.gc(); // 垃圾回收器進行回收
}
           

軟引用的這種特性,比較适合記憶體敏感的場景,做高速緩存。在某些場景下,比如,系統記憶體不是很足的情況下,可以使用軟引用,GC 會自動回收,再次擷取對象的時候,可以對緩存對象進行重建,而又不影響使用。比如:

//建立一個緩存内容cache
String cache = new String("abc");

//進行軟引用處理
SoftReference<String> softRef=new SoftReference<String>(cache);

//判斷是否被垃圾回收器回收
if(softRef.get()!=null){
	//還沒有被回收器回收,直接擷取
	cache = (String) softRef.get();
}else{
	//由于記憶體吃緊,是以對軟引用的對象回收了
	//重建緩存對象
	cache = new String("abc");
	SoftReference<String> softRef = new SoftReference<String>(cache);
}
           
2.1.3、弱引用

WeakReference

指向的對象,屬于弱引用,如下:

String str=new String("abc");

//弱引用
WeakReference<String> abcWeakRef = new WeakReference<String>(str);
           

弱引用與軟引用的差別在于:具有弱引用的對象擁有更短暫的生命周期。

在垃圾回收器線程掃描它所管轄的記憶體區域的過程中,一旦發現了隻具有弱引用的對象,不管目前記憶體空間足夠與否,都會回收它的記憶體。不過,由于垃圾回收器是一個優先級很低的線程,是以不一定會很快發現那些隻具有弱引用的對象。

當垃圾回收器進行掃描回收時,等價于:

str = null;
System.gc();
           

如果這個對象是偶爾的使用,并且希望在使用時随時就能擷取到,但又不想影響此對象的垃圾收集,那麼你應該用 WeakReference 來記住此對象。

同樣的,弱引用對象進入垃圾回收器,Java虛拟機就會把這個弱引用加入到與之關聯的

引用隊列

中,GC 進行回收處理。

2.1.4、虛引用

PhantomReference

指向的對象,屬于虛引用。

虛引用與軟引用和弱引用的一個差別在于:虛引用必須和引用隊列聯合使用,如下:

String str=new String("abc");

//建立引用隊列
ReferenceQueue<String> queue = new ReferenceQueue<String>();

//建立虛引用
PhantomReference<String> phantomReference = new PhantomReference<String>(str, queue);
           

虛引用,顧名思義,就是形同虛設,與其他幾種引用都不同,虛引用并不會決定對象的生命周期。如果一個對象僅持有虛引用,那麼它就和沒有任何引用一樣,在任何時候都可能被垃圾回收器回收。

當垃圾回收器準備回收一個對象時,如果發現它是虛引用,就會在回收對象的記憶體之前,把這個虛引用加入到與之關聯的引用隊列中,GC 進行回收處理。

2.1.5、總結

Java 4中引用的級别由高到低依次為:強引用 > 軟引用 > 弱引用 > 虛引用。

用一張圖來看一下他們之間在垃圾回收時的差別:

深入淺出的分析 WeakHashMap

再次回到本文要講的 WeakHashMap!

WeakHashMap 内部是通過弱引用來管理 entry 的,弱引用的特性對應到 WeakHashMap 上意味着什麼呢?将一對 key, value 放入到 WeakHashMap 裡,随時都有可能被 GC 回收。

下面,咱們一起來看看 WeakHashMap 的具體實作。

三、常用方法介紹

3.1、put方法

put 方法是将指定的 key, value 對添加到 map 裡,存儲結構類似于 HashMap;

不同的是,WeakHashMap 中存儲的 Entry 繼承自 WeakReference,實作了弱引用。

打開源碼如下:

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

WeakHashMap 中存儲的 Entry,源碼如下:

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) {
			  
			//将key進行弱引用處理
            super(key, queue);
            this.value = value;
            this.hash  = hash;
            this.next  = next;
        }
		......
}
           

需要注意的是,Entry 中

super(key, queue)

,傳入的是

key

,是以

key

才是進行弱引用的,

value

是直接強引用關聯在

this.value

中,

System.gc()

時,對

key

進行了回收,而

value

依然保持。

value

是何時被清除的呢?

閱讀源碼,可以看到,調用

getTable()

函數,對調用

expungeStaleEntries()

函數,該方法對 jvm 要回收的的 entry(quene 中) 進行周遊,并将 entry 的 value 設定為空,進行記憶體回收。

private Entry<K,V>[] getTable() {
        expungeStaleEntries();
        return table;
}
           

expungeStaleEntries()

函數,源碼如下:

private void expungeStaleEntries() {
        for (Object x; (x = queue.poll()) != null; ) {
            synchronized (queue) {
                    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;
						//将value設定為null,友善GC回收
                        e.value = null; // Help GC
                        size--;
                        break;
                    }
                    prev = p;
                    p = next;
                }
            }
        }
}
           

是以效果是 key 在 GC 的時候被清除,value 在 key 清除後,通路數組内容的時候進行清除!

3.2、get方法

get 方法根據指定的 key 值傳回對應的 value。

源碼如下:

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) {
			//通過key,進行hash值和equals判斷
            if (e.hash == h && eq(k, e.get()))
                return e.value;
            e = e.next;
        }
        return null;
}
           

同樣的,get 方法在判斷對象之前,也調用了

getTable()

函數,同時,也調用了

expungeStaleEntries()

函數,是以,可能通過 key 擷取元素的時候,得到空值;如果 key 沒有被 GC 回收,那麼就傳回對應的 value。

3.3、remove方法

remove 的作用是通過 key 删除對應的元素。

public V remove(Object key) {
        Object k = maskNull(key);
        int h = hash(k);
		
		//通路數組内容
        Entry<K,V>[] tab = getTable();
        int i = indexFor(h, tab.length);
        Entry<K,V> prev = tab[i];
        Entry<K,V> e = prev;
		
		//循環連結清單,通過key,進行hash值和equals判斷
        while (e != null) {
            Entry<K,V> next = e.next;
            if (h == e.hash && eq(k, e.get())) {
                modCount++;
                size--;
				//找到之後,将連結清單後節點向前移動
                if (prev == e)
                    tab[i] = next;
                else
                    prev.next = next;
                return e.value;
            }
            prev = e;
            e = next;
        }

        return null;
}
           

同樣的,remove 方法在判斷對象之前,也調用了

getTable()

expungeStaleEntries()

函數,是以,可能通過 key 擷取元素的時候,可能被垃圾回收器回收,得到空值。

四、總結

WeakHashMap 跟普通的 HashMap 不同,在存儲資料時,

key

被設定為

弱引用類型

,而

弱引用類型

在 java 中,可能随時被 jvm 的 gc 回收,是以再次通過擷取對象時,可能得到空值,而

value

是在通路數組内容的時候,進行清除。

可能很多人覺得這樣做很奇葩,其實不然,WeekHashMap 的這個特點特别适用于需要緩存的場景。

在緩存場景下,由于系統記憶體是有限的,不能緩存所有對象,可以使用 WeekHashMap 進行緩存對象,即使緩存丢失,也可以通過重新計算得到,不會造成系統錯誤。

五、參考

1、JDK1.7&JDK1.8 源碼

2、知乎 - CarpenterLee - 淺談WeakHashMap

3、csdn - Vander丶 - Java四種引用

4、csdn - java-er - Java四種引用

作者:程式員志哥

出處:www.pzblog.cn

資源:微信搜【Java極客技術】關注我,回複 【cccc】有我準備的一線程式必備計算機書籍、大廠面試資料和免費電子書。 一共24G的資料,希望可以幫助大家提升技術和能力。