一、Reference類型(除強引用)
可以理解為Reference的直接子類都是由jvm定制化處理的,因此在代碼中直接繼承于Reference類型沒有任何作用.只能繼承于它的子類,相應的子類類型包括以下幾種.(忽略沒有在java中使用的,如jnireference)
SoftReference
WeakReference
FinalReference
PhantomReference
上面的引用類型在相應的javadoc中也有提及.FinalReference專門為finalize方法設計,另外幾個也有特定的應用場景.其中softReference用在內存相關的緩存當中,weakReference用在與回收相關的大多數場景.phantomReference用在與包裝對象回收回調場景當中(比如資源泄漏檢測).
可以直接在ide中查看幾個類型的子類信息,即可了解在大多數框架中,都是通過繼承相應的類型用在什么場景當中,以便于我們實際進行選型處理.
二、Reference構造函數
其內部提供2個構造函數,一個帶queue,一個不帶queue.其中queue的意義在于,我們可以在外部對這個queue進行監控.即如果有對象即將被回收,那么相應的reference對象就會被放到這個queue里.我們拿到reference,就可以再作一些事務.
而如果不帶的話,就只有不斷地輪訓reference對象,通過判斷里面的get是否返回null(phantomReference對象不能這樣作,其get始終返回null,因此它只有帶queue的構造函數).這兩種方法均有相應的使用場景,取決于實際的應用.如weakHashMap中就選擇去查詢queue的數據,來判定是否有對象將被回收.而ThreadLocalMap,則采用判斷get()是否為null來作處理.
相應的構造函數如下所示:
1
2
3
4
5
6
7
8
|
Reference(T referent) { this (referent, null ); } Reference(T referent, ReferenceQueue<? super T> queue) { this .referent = referent; this .queue = (queue == null ) ? ReferenceQueue.NULL : queue; } |
這里面的NULL隊列,即可以理解為不需要對其隊列中的數據作任何處理的隊列.并且其內部也不會存取任何數據.
在上面的對象中,referent表示其引用的對象,即我們在構造的時候,需要被包裝在其中的對象.對象即將被回收的定義即此對象除了被reference引用之外沒有其它引用了(并非確實沒有被引用,而是gcRoot可達性不可達,以避免循環引用的問題).
queue即是對象即被回收時所要通知的隊列,當對象即被回收時,整個reference對象(而不是被回收的對象)會被放到queue里面,然后外部程序即可通過監控這個queue拿到相應的數據了.
三、ReferenceQueue及Reference引用鏈
這里的queue名義上是一個隊列,但實際內部并非有實際的存儲結構,它的存儲是依賴于內部節點之間的關系來表達.可以理解為queue是一個類似于鏈表的結構,這里的節點其實就是reference本身.可以理解為queue為一個鏈表的容器,其自己僅存儲當前的head節點,而后面的節點由每個reference節點自己通過next來保持即可.
Reference狀態值
每個引用對象都有相應的狀態描述,即描述自己以及包裝的對象當前處于一個什么樣的狀態,以方便進行查詢,定位或處理.
1、Active:活動狀態,即相應的對象為強引用狀態,還沒有被回收,這個狀態下對象不會放到queue當中.在這個狀態下next為null,queue為定義時所引用的queue.
2、Pending:準備放入queue當中,在這個狀態下要處理的對象將挨個地排隊放到queue當中.在這個時間窗口期,相應的對象為pending狀態.不管什么reference,進入到此狀態的,即可認為相應的此狀態下,next為自己(由jvm設置),queue為定義時所引用的queue.
3、Enqueued:相應的對象已經為待回收,并且相應的引用對象已經放到queue當中了.準備由外部線程來詢循queue獲取相應的數據.此狀態下,next為下一個要處理的對象,queue為特殊標識對象ENQUEUED.
4、Inactive:即此對象已經由外部從queue中獲取到,并且已經處理掉了.即意味著此引用對象可以被回收,并且對內部封裝的對象也可以被回收掉了(實際的回收運行取決于clear動作是否被調用).可以理解為進入到此狀態的肯定是應該被回收掉的.
jvm并不需要定義狀態值來判斷相應引用的狀態處于哪個狀態,只需要通過計算next和queue即可進行判斷.
四、ReferenceQueue#head
始終保存當前隊列中最新要被處理的節點,可以認為queue為一個后進先出的隊列.當新的節點進入時,采取以下的邏輯
1
|
newE.next = head;head=newE; |
然后,在獲取的時候,采取相應的邏輯
1
|
tmp = head;head=tmp.next; return tmp; |
五、Reference#next
即描述當前引用節點所存儲的下一個即將被處理的節點.但next僅在放到queue中才會有意義.為了描述相應的狀態值,在放到隊列當中后,其queue就不會再引用這個隊列了.而是引用一個特殊的ENQUEUED.因為已經放到隊列當中,并且不會再次放到隊列當中.
六、Reference#referent
即描述當前引用所引用的實際對象,正如在注解中所述,其會被仔細地處理.即此什么什么時候會被回收,如果一旦被回收,則會直接置為null,而外部程序可通過通過引用對象本身(而不是referent)了解到,回收行為的產生.
七、ReferenceQueue#enqueue 待處理引用入隊
此過程即在reference對象從active->pending->enqued的過程. 此方法為處理pending狀態的對象為enqued狀態.相應的過程即為之前的邏輯,即將一個節點入隊操作,相應的代碼如下所示.
1
2
3
4
5
|
r.queue = ENQUEUED; r.next = (head == null ) ? r : head; head = r; queueLength++; lock.notifyAll(); |
最后的nitify即通知外部程序之前阻塞在當前隊列之上的情況.(即之前一直沒有拿到待處理的對象)
八、Reference#tryHandlePending
即處理reference對象從active到pending狀態的變化.在Reference對象內部,有一個static字段,其相應的聲明如下:
1
2
3
4
5
6
|
/* List of References waiting to be enqueued. The collector adds * References to this list, while the Reference-handler thread removes * them. This list is protected by the above lock object. The * list uses the discovered field to link its elements. */ private static Reference<Object> pending = null ; |
可以理解為jvm在gc時會將要處理的對象放到這個靜態字段上面.同時,另一個字段discovered,表示要處理的對象的下一個對象.即可以理解要處理的對象也是一個鏈表,通過discovered進行排隊,這邊只需要不停地拿到pending,然后再通過discovered不斷地拿到下一個對象即可.因為這個pending對象,兩個線程都可能訪問,因此需要加鎖處理.
相應的處理過程如下所示:
1
2
3
4
5
6
7
8
9
10
11
12
|
if (pending != null ) { r = pending; // 'instanceof' might throw OutOfMemoryError sometimes // so do this before un-linking 'r' from the 'pending' chain... c = r instanceof Cleaner ? (Cleaner) r : null ; // unlink 'r' from 'pending' chain pending = r.discovered; r.discovered = null ; } //將處理對象入隊,即進入到enqued狀態 ReferenceQueue<? super Object> q = r.queue; if (q != ReferenceQueue.NULL) q.enqueue(r); |
九、Reference#clear
清除引用對象所引用的原對象,這樣通過get()方法就不能再訪問到原對象了.從相應的設計思路來說,既然都進入到queue對象里面,就表示相應的對象需要被回收了,因為沒有再訪問原對象的必要.此方法不會由JVM調用,而jvm是直接通過字段操作清除相應的引用,其具體實現與當前方法相一致.
clear的語義就是將referent置null.
WeakReference對象進入到queue之后,相應的referent為null.
SoftReference對象,如果對象在內存足夠時,不會進入到queue,自然相應的reference不會為null.如果需要被處理(內存不夠或其它策略),則置相應的referent為null,然后進入到queue.
FinalReference對象,因為需要調用其finalize對象,因此其reference即使入queue,其referent也不會為null,即不會clear掉.
PhantomReference對象,因為本身get實現為返回null.因此clear的作用不是很大.因為不管enqueue還是沒有,都不會清除掉.
十、ReferenceHandler enqueue線程
上面提到jvm會將要處理的對象設置到pending對象當中,因此肯定有一個線程來進行不斷的enqueue操作,此線程即引用處理器線程,其優先級為MAX_PRIORITY,即最高.相應的啟動過程為靜態初始化創建,可以理解為當任何使用到Reference對象或類時,此線程即會被創建并啟動.相應的代碼如下所示:
1
2
3
4
5
6
7
8
9
10
11
12
13
|
static { ThreadGroup tg = Thread.currentThread().getThreadGroup(); for (ThreadGroup tgn = tg; tgn != null ; tg = tgn, tgn = tg.getParent()); Thread handler = new ReferenceHandler(tg, "Reference Handler" ); /* If there were a special system-only priority greater than * MAX_PRIORITY, it would be used here */ handler.setPriority(Thread.MAX_PRIORITY); handler.setDaemon( true ); handler.start(); } |
其優先級最高,可以理解為需要不斷地處理引用對象.在通過jstack打印運行線程時,相應的Reference Handler即是指在這里初始化的線程,如下所示:
十一、JVM相關
在上述的各個處理點當中,都與JVM的回收過程相關.即認為gc流程會與相應的reference協同工作.如使用cms收集器,在上述的整個流程當中,涉及到preclean過程,也涉及到softReference的重新標記處理等,同時對reference對象的各種處理也需要與具體的類型相關進行協作.相應的JVM處理,采用C++代碼,因此需要好好地再理一下.
十二、總結
與finalReference對象相同,整個reference和referenceQueue都是一組協同工作的處理組,為保證不同的引用語義,通過與jvm gc相關的流程一起作用,最終實現不同場景,不同引用級別的處理.
另外,由于直接使用referenceQueue,再加上開啟線程去監控queue太過麻煩和復雜.可以參考由google guava實現的 FinalizableReferenceQueue 以及相應的FinalizableReference對象.可以簡化一點點處理過程.以上就是這篇文章的全部內容,希望對大家的學習或者工作帶來一定的幫助。