一、概述
為了更好的理解WeakHashMap的原理,我們有必要先來了解一下WeakReference的作用及實現原理。Java中有一個專門的包java.lang.ref,裡面定義了我們通常所說的幾種引用,具體來說如下:
Reference: 基礎的引用類,是一個抽象類,定義了引用的一些基本方法
SoftReference: 軟引用,軟引用對象在應用出現OOM之前會被回收。
WeakReference: 弱引用,如果一個對象只被弱引用關聯,則垃圾回收器會回收它。
PhantomReference:幻引用,這類用得比較少,個人也不太理解,應該是屬於引用級別最弱的引用。
ReferenceQueue: 引用隊列,垃圾回收器會將已回收的隊象放到這個隊列裡。
除了上述的三類引用之外,還有一類引用叫做強引用,強引用就是我們通常所說的引用,所以這裡java並沒有單獨定義一個引用類來表示。
本文的重點是介紹WeakReference的使用,及其實現原理。
二、WeakReference的使用示例
在介紹其實現原理前,我們先來看一下它的使用效果,根據WeakReference的定義, 如果一個對象只被弱引用所引用,那麼這個對象就是弱可達的,弱可達對象會在系統進行垃圾回收時被回收。
可能這樣說還是不好理解,所以我們以一個示例來說明其使用的效果,代碼如下:
public class TestWeakReference { public static void main(String[] args) throws Exception{ UserInfo userInfo = new UserInfo("Jim Green"); UserInfo anotherUser = userInfo; WeakReference<UserInfo> weakUser = new WeakReference<>(userInfo); System.out.println("\nBefore userInfo is null"); System.out.println("strong ref:" + anotherUser); System.out.println("weak ref:" + weakUser.get()); userInfo = null; System.gc(); System.out.println("\nAfter userInfo is null"); System.out.println("strong ref:" + anotherUser); System.out.println("weak ref:" + weakUser.get()); anotherUser = new UserInfo("Jim White"); System.gc(); System.out.println("\nAfter anotherUser is changed"); System.out.println("strong ref:" + anotherUser); System.out.println("weak ref:" + weakUser.get()); } } class UserInfo { private String name; public UserInfo(String name){ this.name = name; } @Override public String toString() { return "Name is " + name; } }
上述代碼的運行結果如下:
從上面的結果可以看到,剛開始,這三個引用都指向同一個用戶對象Jim Green, 然後我們將原引用置為null,進行垃圾回收,但由於還有一個強引用指向這個Jim Green, 所以這個用戶對象不會被回收,最後,當沒有強引用再指向它了,再做垃圾回收,則Jim Green被回收了。
這個結果也印證了WeakReference的作用,如果其指向的對象沒有被任何強引用指向,則該對象是可以回收的。
下面我們再以圖例的形式來演示這個過程。
三、WeakReference 的實現原理
上面我們詳細演示了使用的方式,接下來我們再說一下其主要實現。
1. UML圖
WeakReference , Reference和ReferenceQueue三者之間的關系如下:
上圖中列出了這三個類的一些主要的屬性和方法。其中WeakReference是抽象類,但是提供了一個引用所需要的基本功能,而WeakReference則只是簡單繼承,並沒有實現任何擴展的功能。
Reference有兩個構造的方法,分別是帶隊列和不帶隊列的,如下:
Reference(T referent) { this(referent, null); } Reference(T referent, ReferenceQueue<? super T> queue) { this.referent = referent; this.queue = (queue == null) ? ReferenceQueue.NULL : queue; }
可見,referent屬性存儲了其所引用的對象,而queue這個字段是可選的,前面說到,queue的作用保存對象將被回收的引用,由垃圾回收器負責往裡面添加,但如果不提供,則沒有這一過程。
2. 狀態說明:
根據類的說明,引用有四種狀態:
Active: 這是創建該引用後的初始狀態,如果該引用對象所引用的對象的可達性發生了變化,則引用本身的狀態將變為Pending或Inactive,取決於這個引用在創建時是否使用了隊列。如果使用了隊列,則狀態將會轉化為Pending,否則直接變為Inactive。
Pending: 由Active轉化而來,處於該狀態的引用在一個公共的pending隊列裡,等待被添加到queue裡去。對於pending隊列的處理由一個優先級比較高的守護線程實時監控處理。如果該引用在初始化時沒有使用queue,則不會被加到pending隊列裡,當然也不可能添加到queue裡。
Enqueued: 由Pending轉化而來,如果pending列表中的引用被正確添加到了queue裡,則引用的狀態為enqueued,如果該引用被從queue隊列裡移除了,則其將變為InActive狀態。同樣,這個前提就是構造時需要指定queue.
InActive: 最後的狀態,處於這個狀態的引用對象實際上已經沒什麼作用了,狀態也不會發生任何改變。
這四種狀態只是一種說明,實際上Reference對象並沒有任何的status字段,不過作為隊列中的節點,它有一個next字段,當狀態為Active時,其next為null,而當其為其它狀態時,next一定不為null,而是指向隊列中的下一個引用,如果其本身就是隊列中的最後一個元素,則next指向其自身。
3. 原理說明:
在介紹了結構和狀態說明後,我們再來對其實現進行分析,這個需要分兩種情況:
1)構造時沒有使用queue:
這種情況比較簡單,狀態轉換只有Active和InActive,不涉及到隊列的操作,當引用所指向的對象沒有任何其它的強引用時,垃圾回收器將會回收該對象,而其狀態也應該會變為InActive.值得注意的時,這樣get()就會直接返回null了。
2)構造函數使用了queue:
這種情況就復雜了,當引用指向的對象沒有其它的強引用時,垃圾回收器會先將其添加到pending隊列裡,而Reference會通過一個公共的守護線程來處理pending隊列裡的引用對象,將其添加到queue隊列中去。
這個過程的處理邏輯如下:
1 private static class ReferenceHandler extends Thread { 2 3 ReferenceHandler(ThreadGroup g, String name) { 4 super(g, name); 5 } 6 7 public void run() { 8 for (;;) { 9 10 Reference r; 11 synchronized (lock) { 12 if (pending != null) { 13 r = pending; //取一個元素 14 Reference rn = r.next;//找隊列中下一個引用對象 15 pending = (rn == r) ? null : rn; //如果是最後一個元素,則pending隊列置空 16 r.next = r; //改變其next 17 } else { 18 try { 19 lock.wait(); //等待 20 } catch (InterruptedException x) { } 21 continue; 22 } 23 } 24 25 // Fast path for cleaners 26 if (r instanceof Cleaner) { 27 ((Cleaner)r).clean(); 28 continue; 29 } 30 31 ReferenceQueue q = r.queue; 32 if (q != ReferenceQueue.NULL) q.enqueue(r);//調用隊列中的入隊方法 33 } 34 } 35 }
上面的方法會一直處理pending隊列直到為null,之後將處於wait狀態,那麼問題來了,當pending隊列再次不為空時,這個線程需要被喚醒。往pending隊列裡加引用對象,並執行喚醒操作的工作是誰來完成的呢?答案是由垃圾回收器在回收引用指向的對象時來調用的。
所以說,是垃圾回收器完成了引用對象從Active到Pending的轉換,而引用對象的線程完成了引用對象由Pending到Enqueued的轉換。
接下來我們再了解下ReferenceQueue中入隊的處理過程:
1 boolean enqueue(Reference<? extends T> r) { /* Called only by Reference class */ 2 synchronized (r) { 3 if (r.queue == ENQUEUED) return false; 4 synchronized (lock) { 5 r.queue = ENQUEUED; 6 r.next = (head == null) ? r : head; 7 head = r; 8 queueLength++; 9 if (r instanceof FinalReference) { 10 sun.misc.VM.addFinalRefCount(1); 11 } 12 lock.notifyAll(); 13 return true; 14 } 15 } 16 }
從其實現邏輯可以看出,每次入隊是從頭入隊,入隊後,更新其queue屬性,這樣可以防止多次入隊。
對應還有出隊的功能,這個就不再分析了。
queue的入隊我們介紹完了,但是垃圾回收器不會對這個隊列做出隊操作,那麼這個隊列有什麼用呢?JDK中有一段對其的描述如下:
“在創建引用對象時,通過向 引用隊列 注冊 一個適當的引用對象,程序可以請求在對象可到達性更改時獲得通知。在垃圾回收器確定引用的可到達性已經更改為對應於引用類型的值之後的某一時間,它會將引用添加到相關的隊列中。此時,該引用被認為是 已加入隊列的。通過輪詢或阻塞,直到獲得了引用,程序才可以從隊列中移除引用。引用隊列是通過
類實現的。”ReferenceQueue
所以,這個queue是提供給應用程序通知用的,也就是說,程序可以通過監聽這個隊列,來獲悉哪些弱引用所指向的對象已經被回收了,進而程序可以做相應的處理。至於如何監控,可以參考Reference類中對於pending隊列的處理方式。
四、總結
至此,我們對Reference及ReferenceQueue的實現方式做了一個完整的介紹,下面再總結一下:
1. WeakReference在創建時需要關聯到另一個對象,如果該對象沒有別的普通(強)引用,則該對象將會被垃圾回收器回收。
2. Reference在定義時可以指定一個類型為ReferenceQueue的隊列,該隊列的作用則是存儲那些關聯對象已經被回收了的Reference對象,以供應用程序監聽,但如何處理由應用程序自己決定。
3. WeakReference繼承於Reference,但自身並未提供擴展的功能。