【集合系列】- 深入淺出的分析 WeakHashMap
一、摘要
在集合系列的第一章,咱們瞭解到,Map 的實現類有 HashMap、LinkedHashMap、TreeMap、IdentityHashMap、WeakHashMap、Hashtable、Properties 等等。
本文主要從資料結構和演算法層面,探討 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 版本開始,把物件的引用分為四種級別,從而使程式更加靈活的控制物件的生命週期。這四種級別由高到低依次為:強引用、軟引用、弱引用和虛引用。
用表格整理之後,各個引用型別的區別如下:
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 內部是通過弱引用來管理 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