JDK1.8原始碼逐字逐句帶你理解WeakHashMap底層
引言
WeakHashMap其實也是java不常見的東西,但是和linkedHashMap一樣,有它自己獨特的功能。在本篇博文中我會用例子詳細介紹它獨有的屬性,同時會對照原始碼來解釋為什麼它具備這樣的功能。在知識點中會擴充套件關於引用的相關知識,幫助後面的理解。筆者目前整理的一些blog針對面試都是超高頻出現的。大家可以點選連結:http://blog.csdn.net/u012403290
技術點
1、java中的引用
關於java中的引用,其實我在“GC-談談“生死””這篇文章中就詳細介紹過引用的概念,從原來的粗獷定義到現在的定義(http://blog.csdn.net/u012403290/article/details/65698856
2、引用佇列ReferenceQueue
根據本人的理解,引用佇列就相當於一個電話簿一樣的東西,用於監聽和管理在引用物件
Object o1 = new Object();
Integer o2 = new Integer((int) o1);
比如說上面兩段程式碼,在我們看來如果o2物件不被回收的話,o1永遠都不可能被回收。但是在引用(Reference)中,存在這麼一個情況:如果o1物件除了在o2中有引用之外沒有別的地方存在引用,那麼就可以回收o1。然後當這個o1被回收之後,我們就需要把o2放入引用佇列中,所以引用佇列(ReferenceQueue)就是Reference的監聽器。在WeakHashMap中就是通過ReferenceQueue來反向處理map中的資料,如果物件被回收了,那麼就需要把map中的對應資料移除。
一個例子
或許對於上面的介紹,很多人都看不懂的,接下來我先用HashMap建立一個例子幫大家理解:
package com.brickworkers;
import java.lang.ref.ReferenceQueue;
import java.lang.ref.WeakReference;
import java.util.HashMap;
import java.util.Map;
public class ReferenceTest {
private static final int _1MB = 1024*1024;//設定大小為1MB
public static void main(String[] args) throws InterruptedException {
ReferenceQueue<Object> referenceQueue = new ReferenceQueue<Object>();//用引用佇列進行監控引用的回收情況
Object value = new Object();
Map<Object, Object> map = new HashMap<Object, Object>();
for (int i = 0; i < 100; i++) {//迴圈100次把資料插入到弱應用中(WeakReference), 同時把弱引用作為key存入HashMap
byte[] bytes = new byte[_1MB];
//每個引用中都有關聯引用佇列(referenceQueue)的構造器,用引用佇列監聽回收情況
//如此,那麼每次WeakReference中的bytes被回收之後,那麼這個weakReference物件就會放入引用佇列
WeakReference<byte[]> weakReference = new WeakReference<byte[]>(bytes, referenceQueue);
map.put(weakReference, value);
}
Thread thread = new Thread(new Runnable() {//執行緒通過呼叫引用佇列的情況檢視那些物件被回收
@SuppressWarnings("unchecked")
public void run() {
try {
int cnt = 0;
WeakReference<byte[]> k;
while ((k = (WeakReference<byte[]>) referenceQueue.remove()) != null) {//返回被回收物件的引用(注意本例中被回收的是bytes)
System.out.println((cnt++)+"回收了"+k);
System.out.println("map的size = " + map.size());//用於監控map的儲存數量有沒有發生變化
}
} catch (Exception e) {
// TODO: handle exception
}
}
});
thread.start();
}
}
//擷取一部分輸出:
// 53回收了[email protected]
// map的size = 100
// 54回收了[email protected]
// map的size = 100
// 55回收了[email protected]
// map的size = 100
// 56回收了[email protected]
// map的size = 100
// 57回收了[email protected]
// map的size = 100
// 58回收了[email protected]
// map的size = 100
注意,回收的物件是bytes,並不是weakReference, 這也是為什麼HashMap中的資料長度並沒有發生變化的原因。我們再梳理一下執行流程:
1、bytes物件存入到weakReference物件中。
2、weakReference物件作為key,一個Object作為值存入HashMap中
2、GC回收了bytes物件,這個時候就要把引用這個物件的weakReference物件儲存到ReferenceQueue中
3、死迴圈ReferenceQueue, 打印出被回收的物件。
下面就是上來的引用關係:
HashMap ——>weakReference——>byte[],千萬注意被回收的是byte[]物件。
其實,上面這個邏輯就是核心WeakHashMap的實現,WeakHashMap只不過比上述的程式碼多了一步:把引用回收的物件從Map中移除罷了。
WeakHashMap來實現上面例子
package com.brickworkers;
import java.util.Map;
import java.util.WeakHashMap;
public class ReferenceTest {
private static final int _1MB = 1024*1024;//設定大小為1MB
public static void main(String[] args) throws InterruptedException {
Object value = new Object();
Map<Object, Object> map = new WeakHashMap<Object, Object>();
for (int i = 0; i < 100; i++) {//迴圈100次把資料插入WeakHashMap中
byte[] bytes = new byte[_1MB];
map.put(bytes, value);
}
while (true) {//死迴圈監控map大小變化
Thread.sleep(500);//稍稍停頓,效果更直觀
System.out.println(map.size());//列印WeakHashMap的大小
System.gc();//建議系統進行GC
}
}
//擷取一部分輸出:
// 41
// 0
// 0
// 0
}
以上的程式碼就是用WeakHashMap來實現了,你會說為什麼不直接在最上面的程式碼把HashMap改成WeakHashMap就行了呢?不行的!WeakHashMap在類的內部就構建了引用佇列(ReferenceQueue)和弱引用(weakReference ),具體的下面原始碼會介紹到。我們先來分析和解釋一下上面的程式碼,一般人會有2個疑問:
①為什麼我插入100個數據,第一次列印是41呢?
因為在插入的過程中已經觸發過GC了,你可以把size的列印放到迴圈內部,你就會發現原因。同時為什麼是到達41呢?這個和你的記憶體有關係,如果記憶體很富足,它就不會發生GC,而且弱引用是雞肋一般的東西:食之無味,棄之可惜。他們只能存活到下次GC之前。
②為什麼後來又變成0了呢?
因為在列印了大小之後,我建議系統(System.gc())發起一次GC操作,為什麼說是建議呢?因為系統不一定會接收到你 指令就會發生GC的。一旦GC發生,那麼弱引用就會被清除,導致WeakHashMap的大小為0。
同時,值得一提的是,存在WeakHashMap中的資料,並不會平白無故就給你移除了map中的資料,必然是你觸發了一些操作,在上述程式碼中size方法就會觸發這個操作,下面是size的原始碼:
/**
* Returns the number of key-value mappings in this map.
* This result is a snapshot, and may not reflect unprocessed
* entries that will be removed before next attempted access
* because they are no longer referenced.
*/
public int size() {
if (size == 0)
return 0;
expungeStaleEntries();//這個操作就是處理到不存在的引用方法
return size;
}
當然不僅僅在size方法會觸發,下面原始碼介紹我們會講到。
逐字逐句理解WeakHashMap
當然,理解Map相關的,需要你對Map有所瞭解,如果你不是很瞭解請參考一下我寫的關於HashMap的博文:http://blog.csdn.net/u012403290/article/details/65442646。
1、在WeakHashMap中核心成員變數(關於HashMap中已存在的不再贅述):
①引用佇列
/**
* Reference queue for cleared WeakEntries
*/
private final ReferenceQueue<Object> queue = new ReferenceQueue<>();
這個佇列其實就是我們前面研究過的,用於監控物件回收的情況。
②靜態內部類Entry K,V
下面是我擷取的部分原始碼
/**
* The entries in this hash table extend WeakReference, using its main ref
* field as the key.
*/
//繼承了弱引用WeakReference, 同時實現了Map.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;
}
.
.
.//這裡還重寫了一些方法
}
這個靜態內部類也是WeakHashMap的核心,因為它把key值封裝進了弱引用(WeakReference)中,這樣一來,就回到了我們最前面的例子中,GC的時候可以回收掉弱引用物件中引用的物件(很拗口是不是?我自己寫的自己讀都拗口,其實就是真正的key值被弱引用WeakReference包裝了),在原始碼中super實現了弱引用與引用佇列關聯的構造器,這樣引用佇列可以對弱引用進行監控了。
③核心移除map中K,V方法
如此一來,結合最前面的程式碼,我們對WeakHashMap的理解已經基本成型了。接下來,我們要解釋一下為什麼物件回收之後,map中的對應K,V也會被移除,核心就是下面這個方法:
/**
* Expunges stale entries from the table.
*/
private void expungeStaleEntries() {
for (Object x; (x = queue.poll()) != null; ) {//存在物件被GC, 那麼就需要移除map中對應的資料
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) {//如果P節點存在
Entry<K,V> next = p.next;//定義一個next節點指向p的下個節點
if (p == e) {//如果P就是當前節點
if (prev == e)
table[i] = next;//意思就是桶中第一個資料就是需要移除的,直接把第二個節點放到頭節點的位置
else
prev.next = next;//那就把上個節點的下個節點指向p後面的節點
// Must not null out e.next;
// stale entries may be in use by a HashIterator
e.value = null; // Help GC幫助GC,直接刪除e的對應value值
size--;//減少WeakHashMap的大小
break;//結束
}
prev = p;
p = next;
}
}
}
}
或許有小夥伴看原始碼有些吃力,我把上面這段程式碼主要做的事情寫出來:
①:迴圈遍歷引用佇列(queue), 如果發現某個物件被GC了,那麼就開始處理。
②:如果被處理的這個節點是頭節點,那麼直接把該節點的下個節點放到頭節點,然後幫助GC去除value的引用,接著把WeakHashMap的大小減1。
③:如果被處理的這個節點不是頭結點,那麼就需要把這個節點的上個節點中的next指標直接指向當前節點的下個節點。意思就是a->b->c,這個時候要移除b,那麼就變成a->c。然後幫助GC去除value的引用,接著把WeakHashMap的大小減1。
那麼在那些時候出發這個expungeStaleEntries方法呢?查詢原始碼之後就會發現好多方法都會呼叫這個方法:
//size方法
public int size() {
if (size == 0)
return 0;
expungeStaleEntries();//去除被回收的物件
return size;
}
//getTable方法(這個方法是put和get方法的輔助方法)
/**
* Returns the table after first expunging stale entries.
*/
private Entry<K,V>[] getTable() {
expungeStaleEntries();//去除被回收的物件
return table;
}
//resize擴容方法
void resize(int newCapacity) {
Entry<K,V>[] oldTable = getTable();
int oldCapacity = oldTable.length;
if (oldCapacity == MAXIMUM_CAPACITY) {
threshold = Integer.MAX_VALUE;
return;
}
Entry<K,V>[] newTable = newTable(newCapacity);
transfer(oldTable, newTable);
table = newTable;
/*
* If ignoring null elements and processing ref queue caused massive
* shrinkage, then restore old table. This should be rare, but avoids
* unbounded expansion of garbage-filled tables.
*/
if (size >= threshold / 2) {
threshold = (int)(newCapacity * loadFactor);
} else {
expungeStaleEntries();//去除被回收的物件
transfer(newTable, oldTable);
table = oldTable;
}
}
//get方法(基於上面說的getTable方法)
public V get(Object key) {
Object k = maskNull(key);
int h = hash(k);
Entry<K,V>[] tab = getTable();//getTable中包裝了expungeStaleEntries方法
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;
}
//put方法(基於上面說的getTable方法)
public V put(K key, V value) {
Object k = maskNull(key);
int h = hash(k);
Entry<K,V>[] tab = getTable();//getTable中包裝了expungeStaleEntries方法
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;
}
}
這個方法是滲透在很多方法裡面的,這裡就不繼續一一列舉了,同時關於WeakHashMap的新增(put),獲取(get), 擴容(resize)這裡就不一一介紹了,如果不清楚的,請去檢視我寫的HashMap詳解,裡面我又詳細介紹過。
如果你覺得我那裡說的不對,或者有更好的解釋,歡迎留言交流。我說的並不一定對,可能只是一個簡單的指導作用,大家可以自己深入研究一下,或許會有別開新面的收穫哦。