Java WeakHashMap 原始碼解析

liujiacai發表於2016-04-19

前面把基於特定資料結構的Map介紹完了,它們分別利用了相應資料結構的特點來實現特殊的目的,像HashMap利用雜湊表的快速插入、查詢實現O(1)的增刪改查,TreeMap則利用了紅黑樹來保證key的有序性的同時,使得增刪改查的時間複雜度為O(log(n))

今天要介紹的WeakHashMap並沒有基於某種特殊的資料結構,它的主要目的是為了優化JVM,使JVM中的垃圾回收器(garbage collector,後面簡寫為 GC)更智慧的回收“無用”的物件。

引用型別

WeakHashMap與其他 Map 最主要的不同之處在於其 key 是弱引用型別,其他 Map 的 key 均為強引用型別,說到這裡,必須強調下:Java 中,引用有四種型別,分別為:強(strong)引用、軟(soft)引用、弱(weak)引用、虛(phantom,本意為幽靈)引用。我相信對於 Java 初學者來說,不一定聽過這幾種引用類似,下面先介紹下這幾種型別。

強引用

這是最常用的引用型別,在執行下面的語句時,變數 o 即為一個強引用。

Object o = new Object();

強引用指向的物件無論在何時,都不會被GC 清理掉。

一般來說,對於常駐類應用(比如server),隨著時間的增加,所佔用的記憶體往往會持續上升,如果程式中全部使用強引用,那麼很容易造成記憶體洩漏,最終導致Out Of Memory (OOM),所以 Java 中提供了除強引用之外的其他三種引用,它們全部位於java.lang.ref包中,下面一一介紹。

java.lang.ref.Reference

java.lang.ref.Reference 為 軟(soft)引用、弱(weak)引用、虛(phantom)引用的父類。

Reference類繼承關係
下面分析下Reference的原始碼(其他三種引用都是其子類,區分不是很大)。

建構函式

//referent 為引用指向的物件
Reference(T referent) {
    this(referent, null);
}
//ReferenceQueue物件,可以簡單理解為一個佇列
//GC 在檢測到appropriate reachability changes之後,
//會把引用物件本身新增到這個queue中,便於清理引用物件本身
Reference(T referent, ReferenceQueue<? super T> queue) {
    this.referent = referent;
    this.queue = (queue == null) ? ReferenceQueue.NULL : queue;
}

如果我們在建立一個引用物件時,指定了ReferenceQueue,那麼當引用物件指向的物件達到合適的狀態(根據引用型別不同而不同)時,GC 會把引用物件本身新增到這個佇列中,方便我們處理它,因為

引用物件指向的物件 GC 會自動清理,但是引用物件本身也是物件(是物件就佔用一定資源),所以需要我們自己清理。

舉個例子:

SoftReference<String> ss = new SoftReference<String>("abc" , queue);

ss 為軟引用,指向abc這個物件,abc 會在一定時機被 GC 自動清理,但是ss物件本身的清理工作依賴於queue,當ss出現在queue中時,說明其指向的物件已經無效,可以放心清理ss了。

從上面的分析大家應該對Reference類有了基本的認識,但是上面也提到了,不同的引用,新增到ReferenceQueue的時機是不一樣。下面介紹具體引用時再進行說明。
這裡有個問題,如果建立引用物件是沒有指定ReferenceQueue,引用物件會怎麼樣呢?這裡需要了解Reference類內部的四種狀態。

四種狀態

每一時刻,Reference物件都處於下面四種狀態中。這四種狀態用Reference的成員變數queuenext(類似於單連結串列中的next)來標示。

ReferenceQueue<? super T> queue;
Reference next;

Active。新建立的引用物件都是這個狀態,在 GC 檢測到引用物件已經到達合適的reachability時,GC 會根據引用物件是否在建立時制定ReferenceQueue引數進行狀態轉移,如果指定了,那麼轉移到Pending,如果沒指定,轉移到Inactive。在這個狀態中

//如果構造引數中沒指定queue,那麼queue為ReferenceQueue.NULL,否則為構造引數中傳遞過來的queue
queue = ReferenceQueue || ReferenceQueue.NULL
next = null

Pending。pending-Reference列表中的引用都是這個狀態,它們等著被內部執行緒ReferenceHandler處理(會呼叫ReferenceQueue.enqueue方法)。沒有註冊的例項不會進入這個狀態。在這個狀態中

//構造引數引數中傳遞過來的queue
queue = ReferenceQueue
next = 該queue中的下一個引用,如果是該佇列中的最後一個,那麼為this

Enqueued。呼叫ReferenceQueue.enqueued方法後的引用處於這個狀態中。沒有註冊的例項不會進入這個狀態。在這個狀態中

queue = ReferenceQueue.ENQUEUED
next = 該queue中的下一個引用,如果是該佇列中的最後一個,那麼為this

Inactive。最終狀態,處於這個狀態的引用物件,狀態不會在改變。在這個狀態中

queue = ReferenceQueue.NULL
next = this

有了這些約束,GC 只需要檢測next欄位就可以知道是否需要對該引用物件採取特殊處理

  • 如果nextnull,那麼說明該引用為Active狀態
  • 如果next不為null,那麼 GC 應該按其正常邏輯處理該引用。

我自己根據Reference.ReferenceHandler.runReferenceQueue.enqueue這兩個方法,畫出了這四種狀態的轉移圖,供大家參考:

Reference狀態轉移圖
要理解這個狀態 GC 到底做了什麼事,需要看 JVM 的程式碼,我這裡時間、能力都不夠,就不獻醜了,後面有機會再來填坑。
對於一般程式設計師來說,這四種狀態完全可以不用管。最後簡單兩句話總結上面的四種狀態:
  1. 如果建構函式中指定了ReferenceQueue,那麼事後程式設計師可以通過該佇列清理引用
  2. 如果建構函式中沒有指定了ReferenceQueue,那麼 GC 會自動清理引用

get

呼叫Reference.get方法可以得到該引用指向的物件,但是由於指向的物件隨時可能被 GC 清理,所以即使在同一個執行緒中,不同時刻的呼叫可能返回不一樣的值。

軟引用(soft reference)

軟引用“儲存”物件的能力稍遜於強引用,但是高於弱引用,一般用來實現memory-sensitive caches。

軟引用指向的物件會在程式即將觸發OOM時被GC 清理掉,之後,引用物件會被放到ReferenceQueue中。

弱引用(weak reference)

軟引用“儲存”物件的能力稍遜於弱引用,但是高於虛引用,一般用來實現canonicalizing mapping,也就是本文要講的WeakHashMap

當弱引用指向的物件只能通過弱引用(沒有強引用或弱引用)訪問時,GC會清理掉該物件,之後,引用物件會被放到ReferenceQueue中。

虛引用(phantom reference)

虛引用是“儲存”物件能力最弱的引用,一般用來實現scheduling pre-mortem cleanup actions in a more flexible way than is possible with the Java finalization mechanism

呼叫虛引用的get方法,總會返回null,與軟引用和弱引用不同的是,虛引用被enqueued時,GC 並不會自動清理虛引用指向的物件,只有當指向該物件的所有虛引用全部被清理(enqueued後)後或其本身不可達時,該物件才會被清理。

WeakHashMap.Entry

上面介紹了很多引用的知識點,其實WeakHashMap本身沒什麼好說的,只要是把引用的作用與使用場景搞清楚了,再來分析基於這些引用的物件就會很簡單了。
WeakHashMapHashMap的簽名與建構函式一樣,這裡就不介紹了,這裡重點介紹下Entry這個內部物件,因為其儲存具體key-value對,所以把它弄清楚了,其他的就問題不大了。

/**
  * The entries in this hash table extend WeakReference, using its main ref
  * field as the key.
  */
 private static class Entry<K,V> extends WeakReference<Object> implements Map.Entry<K,V> {
     V value;
     int hash;
     Entry<K,V> next;
     /**
      * Creates new entry.
      */
     Entry(Object key, V value,
           ReferenceQueue<Object> queue,
           int hash, Entry<K,V> next) {
         //這裡把key傳給了父類WeakReference,說明key為弱引用(沒有顯式的 this.key = key)
         //所有如果key只有通過弱引用訪問時,key會被 GC 清理掉
         //同時該key所代表的Entry會進入queue中,等待被處理
         //還可以看到value為強引用(有顯式的 this.value = value ),但這並不影響
         //後面可以看到WeakHashMap.expungeStaleEntries方法是如何清理value的
         super(key, queue);
         this.value = value;
         this.hash  = hash;
         this.next  = next;
     }
     @SuppressWarnings("unchecked")
     //在獲取key時需要unmaskNull,因為對於null的key,是用WeakHashMap的內部成員屬性來表示的
     public K getKey() {
         return (K) WeakHashMap.unmaskNull(get());
     }
     public V getValue() {
         return value;
     }
     public V setValue(V newValue) {
         V oldValue = value;
         value = newValue;
         return oldValue;
     }
     public boolean equals(Object o) {
         if (!(o instanceof Map.Entry))
             return false;
         Map.Entry<?,?> e = (Map.Entry<?,?>)o;
         K k1 = getKey();
         Object k2 = e.getKey();
         if (k1 == k2 || (k1 != null && k1.equals(k2))) {
             V v1 = getValue();
             Object v2 = e.getValue();
             if (v1 == v2 || (v1 != null && v1.equals(v2)))
                 return true;
         }
         return false;
     }
     public int hashCode() {
         K k = getKey();
         V v = getValue();
         return ((k==null ? 0 : k.hashCode()) ^
                 (v==null ? 0 : v.hashCode()));
     }
     public String toString() {
         return getKey() + "=" + getValue();
     }
 }

WeakHashMap.expungeStaleEntries

/**
 * Reference queue for cleared WeakEntries
 */
// 所有Entry在構造時都傳入該queue
private final ReferenceQueue<Object> queue = new ReferenceQueue<>();
/**
 * Expunges stale entries from the table.
 */
private void expungeStaleEntries() {
    for (Object x; (x = queue.poll()) != null; ) {
        synchronized (queue) {
            // e 為要清理的物件
            @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 迴圈遍歷衝突鏈
            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,來幫助 GC 回收強引用的value
                    e.value = null; // Help GC
                    size--;
                    break;
                }
                prev = p;
                p = next;
            }
        }
    }
}

知道了expungeStaleEntries方法的作用,下面看看它是何時被呼叫的

expungeStaleEntries呼叫鏈
可以看到,在對WeakHashMap進行增刪改查時,都呼叫了expungeStaleEntries方法。

實戰

上面說了,下面來個具體的例子幫助大家消化

import java.util.WeakHashMap;
class KeyHolder {
    @Override
    protected void finalize() throws Throwable {
        System.out.println("I am over from key");
        super.finalize();
    }
}
class ValueHolder {
    @Override
    protected void finalize() throws Throwable {
        System.out.println("I am over from value");
        super.finalize();
    }
}
public class RefTest {
    public static void main(String[] args) {
        WeakHashMap<KeyHolder, ValueHolder> weakMap = new WeakHashMap<KeyHolder, ValueHolder>();
        KeyHolder kh = new KeyHolder();    
        ValueHolder vh = new ValueHolder();

        weakMap.put(kh, vh);
        while (true) {
            for (KeyHolder key : weakMap.keySet()) {
                System.out.println(key + " : " + weakMap.get(key));
            }
            try {
                Thread.sleep(2000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("here...");
            //這裡把kh設為null,這樣一來就只有弱引用指向kh指向的物件
            kh = null;
            System.gc();
        }
    }
}

輸出

KeyHolder@a15670a : ValueHolder@20e1ed5b
here...
I am over from key   //輸出這句話說明,該key對應的Entry已經被 GC 清理
here...
here...
here...
...
...
...

總結

說實話,之前我是沒怎麼了解過引用,更是沒有用過WeakHashMap這個類,這次算是把這個坑給填上了。引用的使用場景應該是在常駐類或消耗記憶體較大應用中才用得上,我自己確實沒怎麼經歷過這種型別的專案,只能現在打好基礎,以後有機會在嘗試。

其實關於引用,本文重點介紹了弱引用的使用場景,其他的沒怎麼介紹,感興趣的可以閱讀參考中給出的連結。

相關文章