Java原始碼分析——java.util工具包解析(四)——四大引用型別以及WeakHashMap類解析
WeakHashMap是Map的一種很獨特的實現,從它的名字可以看出,它是存貯弱引用的對映的,先來複習一下Java中的四大引用型別:
- 強引用:我們使用的大部分引用實際上都是強引用,這是使用最普遍的引用。強引用的物件垃圾回收器絕不會回收它。當記憶體空間不足,jvm寧願丟擲OutOfMemoryError錯誤,使程式異常終止,也不會靠隨意回收具有強引用的物件來解決記憶體不足問題。
- 軟引用:軟引用是當jvm中記憶體不夠的情況下會回收其物件,在記憶體充足的情況下與強引用別無二樣。
- 弱引用:弱引用是隻要GC掃描到了弱引用,那麼它指向的物件就會不管記憶體是否充足都會進行回收。弱引用可以和一個引用佇列(ReferenceQueue)聯合使用,如果弱引用所引用的物件被垃圾回收,jvm就會把這個弱引用加入到與之關聯的引用佇列中。程式可以通過判斷引用佇列中是否已經加入了弱引用,來了解被引用的物件是否將要被垃圾回收。
- 虛引用:虛引用與沒有引用沒有什麼區別,相當於沒有引用指向改物件。虛 引用主要用來跟蹤物件被垃圾回收的活動。虛引用與軟引用和弱引用的一個區別在於:虛引用必須和引用佇列(ReferenceQueue)聯合使用。
通過幾個例子來加深對四大引用型別的理解,先看第一個例子:
String reference="蕾姆";//reference就是一個強引用
SoftReference<String> stringSoftReference=new SoftReference<>(reference) ;
reference=null;
System.gc();
System.out.println(stringSoftReference.get());//輸出:蕾姆
上述"蕾姆"有兩個引用指向它,一個是強引用reference,另外一個是軟引用stringSoftReference。從程式碼中可以看出,即便呼叫了GC,將強引用置為null,也依舊沒有回收其指向的物件。再看下一個,為了瞭解什麼時候物件回收了,重寫finalize方法:
class Rem{
long[] l=new long [10000];
@Override
protected void finalize(){
System.out.println("小蕾姆被回收了");
}
public String toString(){
return "你好,蕾姆";
}
}
public class Test {
public static void main(String args[]) {
WeakReference<Rem> weakReference=new WeakReference<>(new Rem());
SoftReference softReference=new SoftReference(new Rem());
System.gc();
System.out.println(softReference.get());
}
}
//輸出:小蕾姆被回收了
//你好,蕾姆
為了使得GC能找到需要回收的物件,在Rem類裡定義了一個很大的陣列,方便GC進行回收,從程式碼可以看出,弱引用指向的物件是被回收了的,而軟引用則沒有被回收掉。理解了上述程式碼後,再來看看WeakHashMap類,該類繼承自AbstractMap抽象類,與HasnMap類相比,它多一個引用佇列:
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;
Entry<K,V>[] table;
private int size;
private int threshold;
private final float loadFactor;
private final ReferenceQueue<Object> queue = new ReferenceQueue<>();
}
經過上述的引用型別的討論,很容易的得出結論,queue 裡存貯著是需要被GC回收掉的引用。同時需要注意的是HashMap類的鍵是null的話,從null得出的雜湊值是0,所以會存貯在第一個桶中,而WeakHashMap類則不一樣,它給鍵為null值定義了一個Object物件:
private static final Object NULL_KEY = new Object();
private static Object maskNull(Object key) {
return (key == null) ? NULL_KEY : key;
}
static Object unmaskNull(Object key) {
return (key == NULL_KEY) ? null : key;
}
因此,鍵為null時與其他的鍵就沒什麼區別了。其中它的節點定義是繼承了WeakReference,也就是說它裡面儲存的所有節點都是弱引用:
private static class Entry<K,V> extends WeakReference<Object> implements Map.Entry<K,V> {
V value;
final int hash;
Entry<K,V> next;
}
在WeakHashMap的各項操作中,比如get()、put()、size()都間接或者直接呼叫了expungeStaleEntries()方法,以清理弱引用指向的key物件:
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;
//將被清理的鍵對應的值置為null
e.value = null;
//節點數減一
size--;
break;
}
prev = p;
p = next;
}
}
}
}
那麼,在什麼場景下可以用到WeakHashMap類呢?可以再存貯很大的值的場景下使用到WeakHashMap類,比如存貯上萬個節點時,利用HashMap存貯:
class Rem{
int i;
long[] l=new long[10000];
public Rem(int i){
this.i=i;
}
@Override
protected void finalize(){
System.out.println("小蕾姆被回收了");
}
public String toString(){
return "你好,蕾姆";
}
}
public class Test {
public static void main(String args[]) {
HashMap<Integer,Rem> map=new HashMap<>();
// Map<Integer,Rem> map=new WeakHashMap<>();
for (int i=0;i<10000;i++){
map.put(i,new Rem(i));
}
}
}
會報記憶體溢位的異常,而用WeakHashMap類來存貯則不會,但會一直顯示物件被回收掉。從中我們可以得到WeakHashMap的應用場景,這兩段程式碼比較可以看到WeakHashMap的功效,如果在系統中需要一張很大的Map表,Map中的表項作為快取只用,這也意味著即使沒能從該Map中取得相應的資料,系統也可以通過候選方案獲取這些資料。雖然這樣會消耗更多的時間,但是不影響系統的正常執行。在這種場景下,使用WeakHashMap是最合適的。因為WeakHashMap會在系統記憶體範圍內,儲存所有表項,而一旦記憶體不夠,在GC時,沒有被引用的表項又會很快被清除掉,從而避免系統記憶體溢位。