Java還要再學一遍基礎(十一)WeakHashMap詳解
WeakHashMap概述
WeakHashMap是以弱鍵實現的基於雜湊表的儲存對映資料的Map。當JVM對於這些弱鍵所指向的物件進行了清理回收之後,WeakHashMap會自動有效的將被回收了的對映從map中移除。
引用的相關知識
Java中的引用一共分為四種,分別為強引用(Strong Reference),軟引用(Soft Reference),弱引用(Weak Reference)和幻影引用(Phantom Reference)。
可以看到java.lang.ref包中有對應的幾種:
強引用(Strong Reference)
強引用是java預設實現的引用。JVM會盡可能長時間的保留強引用的存在。當沒有任何物件指向它時JVM將會回收。//這是一個強引用 Object obj = new Object();
弱引用(Weak Reference)
弱引用是指當物件沒有任何的強引用存在,在下一次的JVM的gc回收的時候它將會被回收。//這裡是一個強引用 Object obj = new Object(); //new一個弱引用 WeakReference<Object> weakRef = new WeakReference<Object>(obj); //obj = null後不再有強引用 obj = null; //因為jvm的gc時間不確定所以迴圈 while(true){ //weakRef.get()當還未被回收的時候返回該物件,否則返回null
執行結果:
... ... is alive is alive is alive is alive is alive is alive is not alive
- 軟引用(Soft Reference)
軟引用和弱引用基本性質一致,區別就是軟引用JVM只會在虛擬機器記憶體不足的時候才會去回收軟引用,這使得軟引用很適合做快取應用。 - 幻影引用(Phantom Reference)
這種引用型別的get方法無論什麼時候都會返回null。它唯一的作用就是可以用來記錄物件是什麼時候被gc回收的。
問題來了。怎麼去記錄物件的回收呢?
從上面的圖片中包含的class可以看到其中有一個ReferenceQueue的class,翻譯成中文就是引用佇列,用來幹什麼?
原來java提供這個引用佇列,可以把這個隊列當作引數傳到引用的構造器中,那麼之後當物件被回收之後便會做一件事情,就是把被回收的物件放到這個因喲個佇列中去。
利用ReferenceQueue記錄物件被回收的例子:
Object obj = new Object();
ReferenceQueue<Object> queue = new ReferenceQueue<>();
WeakReference<Object> weakRef = new WeakReference<Object>(obj, queue);
System.out.println(obj);
obj = null;
System.gc();
while(true){
//已經被回收
if(weakRef.get() == null){
Object o;
//從佇列中取
if((o = queue.poll()) != null){
System.out.println(o);
break;
}
}
}
執行結果:
java.lang.Object@15db9742
java.lang.ref.WeakReference@6d06d69c
WeakHashMap
有了引用以及弱引用的相關概念並且瞭解HashMap的話WeakHashMap便很容易理解了。本文基於JDK1.8
1. 類定義
public class WeakHashMap<K,V>
extends AbstractMap<K,V>
implements Map<K,V>
跟HashMap一樣繼承自AbstractMap並且實現Map介面,與之不同的是HashMap同時還實現了Cloneable, Serializable介面,這意味著WeakHashMap將不支援克隆和序列化。
2. 屬性
//預設容量
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;
//size
private int size;
//閾值
private int threshold;
//載入因子
private final float loadFactor;
//ReferenceQueue用於記錄gc回收
private final ReferenceQueue<Object> queue = new ReferenceQueue<>();
//修改標誌
int modCount;
可以看到基本上和HashMap沒有什麼太大的出入,明顯的區別則是多了一個ReferenceQueue,很明顯WeakHashMap就是通過這個引用佇列來實現自動清理的。
2. 構造方法
public WeakHashMap(int initialCapacity, float loadFactor) {
if (initialCapacity < 0)
throw new IllegalArgumentException("Illegal Initial Capacity: "+
initialCapacity);
if (initialCapacity > MAXIMUM_CAPACITY)
initialCapacity = MAXIMUM_CAPACITY;
if (loadFactor <= 0 || Float.isNaN(loadFactor))
throw new IllegalArgumentException("Illegal Load factor: "+
loadFactor);
int capacity = 1;
while (capacity < initialCapacity)
capacity <<= 1;
table = newTable(capacity);
this.loadFactor = loadFactor;
threshold = (int)(capacity * loadFactor);
}
//其他構造引數基本與HashMap相同,省略
這裡有一點點與HashMap不同的是,HashMap在確定capacity的演算法上有些不同。HashMap是通五次的無符號右移並或運算
static final int tableSizeFor(int cap) {
int n = cap - 1;
n |= n >>> 1;
n |= n >>> 2;
n |= n >>> 4;
n |= n >>> 8;
n |= n >>> 16;
return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
}
而WeakHashMap則是通過while迴圈實現。
同時HashMap的table陣列並沒有直接在構造器裡面初始化,而是在加入元素之後延遲初始化的。
3. Entry節點
private static class Entry<K,V> extends WeakReference<Object> implements Map.Entry<K,V> {
V value;
final int hash;
Entry<K,V> next;
/**
* Creates new entry.
*/
Entry(Object key, V value,
ReferenceQueue<Object> queue,
int hash, Entry<K,V> next) {
super(key, queue);
this.value = value;
this.hash = hash;
this.next = next;
}
//......
這裡看看構造器,傳入了一個ReferenceQueue,似乎根最開始講的ReferenceQueue記錄gc回收很相似的用法。因為Entry繼承了WeakReference類,所以現在的Entry物件管理的並不是強引用,而是弱引用也就是WeakReference。所以當被回收的時候自然就將會被這個引用佇列所記錄下來。之後只需要進行清理操作即可。
3. 關鍵的方法
get方法。為了能讓每次獲取map中的元素的時候都能從已經自動清理過後的table陣列中獲取,所以get方法中加入了對應了清理操作。
public V get(Object key) { Object k = maskNull(key); int h = hash(k); //這個getTable便是獲取清理後的table Entry<K,V>[] tab = getTable(); int index = indexFor(h, tab.length); Entry<K,V> e = tab[index]; while (e != null) { if (e.hash == h && eq(k, e.get())) return e.value; e = e.next; } return null; }
如何清理的:
private Entry<K,V>[] getTable() { //expunge是抹去的意思 Stale:陳舊的 expungeStaleEntries(); return table; } private void expungeStaleEntries() { //從佇列中出隊遍歷 for (Object x; (x = queue.poll()) != null; ) { synchronized (queue) { @SuppressWarnings("unchecked") Entry<K,V> e = (Entry<K,V>) x; //通過hash找到index int i = indexFor(e.hash, table.length); Entry<K,V> prev = table[i]; Entry<K,V> p = prev; //遍歷連結串列 while (p != null) { //記錄下一個節點next 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 e.value = null; // Help GC size--; break; } //p記錄下一個節點用作遍歷,pref則是p的上一個節點 prev = p; p = next; } } } }
這下就很清楚了,原來是遍歷佇列中被回收的記錄,再在table陣列以及處理hash衝突的連結串列中去找到該記錄並刪除。
初次之外,在resize和size方法呼叫的時候也使用到了上面的expungeStaleEntries方法。這裡不做詳細介紹。hash方法
final int hash(Object k) { int h = k.hashCode(); // This function ensures that hashCodes that differ only by // constant multiples at each bit position have a bounded // number of collisions (approximately 8 at default load factor). h ^= (h >>> 20) ^ (h >>> 12); return h ^ (h >>> 7) ^ (h >>> 4); }
和HashMap不相同,但是有點複雜….
WeakHashMap的特性
- 儲存鍵值對對映。
- key和value可以為null
- 執行緒不安全
- 當鍵被gc回收能夠自動清理map中的對映
- 不支援克隆和序列化