Java WeakHashMap 原始碼解析
前面把基於特定資料結構的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
的原始碼(其他三種引用都是其子類,區分不是很大)。
建構函式
//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
的成員變數queue
與next
(類似於單連結串列中的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
欄位就可以知道是否需要對該引用物件採取特殊處理
- 如果
next
為null
,那麼說明該引用為Active
狀態 - 如果
next
不為null
,那麼 GC 應該按其正常邏輯處理該引用。
我自己根據Reference.ReferenceHandler.run
與ReferenceQueue.enqueue
這兩個方法,畫出了這四種狀態的轉移圖,供大家參考:
對於一般程式設計師來說,這四種狀態完全可以不用管。最後簡單兩句話總結上面的四種狀態:
- 如果建構函式中指定了
ReferenceQueue
,那麼事後程式設計師可以通過該佇列清理引用 - 如果建構函式中沒有指定了
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
本身沒什麼好說的,只要是把引用的作用與使用場景搞清楚了,再來分析基於這些引用的物件就會很簡單了。
WeakHashMap
與HashMap
的簽名與建構函式一樣,這裡就不介紹了,這裡重點介紹下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
方法的作用,下面看看它是何時被呼叫的
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
這個類,這次算是把這個坑給填上了。引用的使用場景應該是在常駐類或消耗記憶體較大應用中才用得上,我自己確實沒怎麼經歷過這種型別的專案,只能現在打好基礎,以後有機會在嘗試。
其實關於引用,本文重點介紹了弱引用的使用場景,其他的沒怎麼介紹,感興趣的可以閱讀參考中給出的連結。
相關文章
- 死磕 java集合之WeakHashMap原始碼分析JavaHashMap原始碼
- WeakHashMap,原始碼解讀HashMap原始碼
- Java Timer原始碼解析(定時器原始碼解析)Java原始碼定時器
- Java——HashMap原始碼解析JavaHashMap原始碼
- Java——ArrayList原始碼解析Java原始碼
- Java——LinkedHashMap原始碼解析JavaHashMap原始碼
- JAVA集合:LinkedList原始碼解析Java原始碼
- Java引用型別與WeakHashMapJava型別HashMap
- Java集合Stack原始碼深入解析Java原始碼
- Java集合之Hashtable原始碼解析Java原始碼
- Java 集合Hashtable原始碼深入解析Java原始碼
- Java集合之ArrayList原始碼解析Java原始碼
- Java集合之LinkedList原始碼解析Java原始碼
- ConcurrentHashMap原始碼解析-Java7HashMap原始碼Java
- ThreadLocal原始碼解析-Java8thread原始碼Java
- HashMap原始碼解析(java1.8.0)HashMap原始碼Java
- Java中的WeakHashMap與類示例JavaHashMap
- React Native 0.55.4 Android 原始碼分析(Java層原始碼解析)React NativeAndroid原始碼Java
- Java集合(6)之 HashMap 原始碼解析JavaHashMap原始碼
- java FileInputStream open0原始碼解析Java原始碼
- Java 集合系列:Vector原始碼深入解析Java原始碼
- 原始碼解析Java Attach處理流程原始碼Java
- JDK原始碼解析之Java SPI機制JDK原始碼Java
- Java原始碼解析 ThreadPoolExecutor 執行緒池Java原始碼thread執行緒
- Java原始碼解析 - ThreadPoolExecutor 執行緒池Java原始碼thread執行緒
- java流的中間操作原始碼解析Java原始碼
- 從JDK原始碼看Java域名解析JDK原始碼Java
- Java HashMap 原始碼逐行解析(JDK1.8)JavaHashMap原始碼JDK
- Java執行緒池ThreadPoolExecutor原始碼解析Java執行緒thread原始碼
- [原始碼解析] 當 Java Stream 遇見 Flink原始碼Java
- Java集合-ArrayList原始碼解析-JDK1.8Java原始碼JDK
- java8LinkedList原始碼閱讀解析Java原始碼
- Java併發之ReentrantLock原始碼解析(三)JavaReentrantLock原始碼
- Java併發之ReentrantReadWriteLock原始碼解析(二)Java原始碼
- Java併發之ReentrantReadWriteLock原始碼解析(一)Java原始碼
- Java併發之ReentrantLock原始碼解析(四)JavaReentrantLock原始碼
- Java併發之Semaphore原始碼解析(一)Java原始碼
- Java併發之Semaphore原始碼解析(二)Java原始碼
- Java併發之ReentrantLock原始碼解析(一)JavaReentrantLock原始碼