Java引用型別
Java中一共有4種引用型別(其實還有一些其他的引用型別比如FinalReference):強引用、軟引用、弱引用、虛引用。其中強引用就是如下的情況:
Object a=new Object();
obj持有的Object物件的引用就是強引用,在Java中並沒有對應的Reference類。
本篇文章主要是分析軟引用、弱引用、虛引用的實現,這三種引用型別都是繼承於Reference這個類,主要邏輯也在Reference中。
Reference是java中的引用類,它用來給普通物件進行包裝,當JVM在GC時,按照引用型別的不同,在回收時執行不同的邏輯。先來看下這個類的繼承體系:
由圖可知,Java存在以下幾種引用:
Java中的引用有:
- 強引用(StrongReference):強引用就是我們平時建立物件,建立陣列時的引用。強引用在任何時候都不會被GC回收掉;
- 軟引用(SoftReference):軟引用是在系統發生OOM之前才被JVM回收掉。軟引用常被用來對於記憶體敏感的快取;
- 弱引用(WeakReference):一旦JVM執行GC,弱引用就會被回收掉;
- 虛引用(PhantomReference):虛引用主要作為其指向referent被回收時的一種通知機制;
- FinalReference:用於收尾機制(finalization) 。
引用例項的幾個狀態
-
Active:當處於Active狀態,GC會特殊處理引用例項,一旦GC檢測到其可達性發生變化,GC就會更改其狀態。此時分兩種情況,如果該引用例項建立時有註冊引用佇列,則會進入Pending狀態,否則會進入Inactive狀態。新建立的引用例項為Active。
-
Pending:當前為pending列表中的一個元素,等待被ReferenceHandler執行緒消費並加入其註冊的引用佇列。如果該引用例項未註冊引用佇列,則永遠不會處於這個狀態。
-
Enqueued:該引用例項建立時有註冊引用佇列並且當前處於入佇列狀態,屬於該引用佇列中的一個元素。當該引用例項從其註冊引用佇列中移除後其狀態變為Inactive。如果該引用例項未註冊引用佇列,則永遠不會處於這個狀態。
-
Inactive:當處於Inactive狀態,無需任何處理,一旦變成Inactive狀態則其狀態永遠不會再發生改變。整體遷移流程圖如下:
整體遷移流程圖如下:
如上的狀態是為了更好的理解而虛擬出來的狀態,並沒有一個欄位來描述狀態,而是通過queue和next欄位來標記的。
- Active:當例項註冊了引用佇列,則queue = ReferenceQueue;當例項沒有註冊引用佇列,那麼queue = ReferenceQueue.NULL。next = null;
- Pending:處在這個狀態下的例項肯定註冊了引用佇列,queue = ReferenceQueue。next = this;
- Enqueued:處在這個狀態下的例項肯定註冊了引用佇列,queue = ReferenceQueue.ENQUEUED,next指向下一個在此佇列中的元素,或者如果佇列中只有當前物件時為當前物件this;
- Inactive:queue =ReferenceQueue.NULL;next = this。
1、Reference
我們先看下Reference類及重要屬性的定義如下:
public abstract class Reference<T> { //引用的物件 private T referent; //回收佇列,由使用者在Reference的建構函式中指定 volatile ReferenceQueue<? super T> queue; //當該引用被加入到queue中的時候,該欄位被設定為queue中的下一個元素,以形成連結串列結構 volatile Reference next; //在GC時,HotSpot底層會維護一個叫DiscoveredList的連結串列,存放的是Reference物件,discovered欄位指向的就是連結串列中的下一個元素,由HotSpot設定 transient private Reference<T> discovered; //進行執行緒同步的鎖物件 static private class Lock { } private static Lock lock = new Lock(); //等待加入queue的Reference物件,在GC時由JVM設定,會有一個java層的執行緒(ReferenceHandler)源源不斷的從pending中提取元素加入到queue private static Reference<Object> pending = null; }
注意Reference指的是引用物件,而Referent指的是所指物件。
一個Reference物件的生命週期如下:
HotSpot在GC時將需要被回收的Reference物件加入到DiscoveredList中,然後將DiscoveredList的元素移動到PendingList中。PendingList的隊首元素由Reference類中的pending屬性持有。
2、ReferenceHandler
ReferenceHandler的程式碼實現如下:
private static class ReferenceHandler extends Thread { ... public void run() { while (true) { tryHandlePending(true); } } } static boolean tryHandlePending(boolean waitForNotify) { Reference<Object> r; Cleaner c; try { synchronized (lock) { if (pending != null) { r = pending; // 如果是Cleaner物件,則記錄下來,下面做特殊處理 c = r instanceof Cleaner ? (Cleaner) r : null; // 指向PendingList的下一個物件 pending = r.discovered; r.discovered = null; } else { // 如果pending為null就先等待,當有物件加入到PendingList中時,jvm會執行notify if (waitForNotify) { lock.wait(); } // retry if waited return waitForNotify; } } } ... // 如果時Cleaner物件,則呼叫clean方法進行資源回收 if (c != null) { c.clean(); return true; } // 將Reference加入到ReferenceQueue,開發者可以通過從ReferenceQueue中poll元素感知到物件被回收的事件。 ReferenceQueue<? super Object> q = r.queue; if (q != ReferenceQueue.NULL) q.enqueue(r); return true; }
源源不斷的從PendingList中獲取元素,然後加入到ReferenceQueue中,開發者可以通過呼叫ReferenceQueue的poll()方法來感知物件被回收的事件。
另外需要注意的是,對於Cleaner型別(繼承自虛引用)的物件會有額外的處理:在其指向的物件被回收時,會呼叫clean()方法,該方法主要是用來做對應的資源回收,在堆外記憶體DirectByteBuffer中就是用Cleaner進行堆外記憶體的回收,這也是虛引用在java中的典型應用,後面會詳細介紹。
3、ReferenceQueue
ReferenceQueue是引用佇列,垃圾收集器在檢測到適當的可達性更改後將已註冊的引用物件追加到該佇列。
public class ReferenceQueue<T> { public ReferenceQueue() { } // 內部類Null類繼承自ReferenceQueue,覆蓋了enqueue方法返回false private static class Null extends ReferenceQueue<Object> { boolean enqueue(Reference<?> r) { return false; } } // ReferenceQueue.NULL和ReferenceQueue.ENQUEUED都是內部類Null的新例項 static final ReferenceQueue<Object> NULL = new Null(); static final ReferenceQueue<Object> ENQUEUED = new Null(); // 靜態內部類,作為鎖物件 private static class Lock { }; private final Lock lock = new Lock();
// 引用連結串列的頭節點 private volatile Reference<? extends T> head; // 引用佇列長度,入隊則增加1,出隊則減少1 private long queueLength = 0; // 入隊操作,只會被Reference例項呼叫 boolean enqueue(Reference<? extends T> r) { // 加鎖 synchronized (lock) { // Check that since getting the lock this reference hasn't already been // enqueued (and even then removed) // 如果引用例項持有的佇列為ReferenceQueue.NULL或者ReferenceQueue.ENQUEUED,則入隊失敗返回false ReferenceQueue<?> queue = r.queue; if ((queue == NULL) || (queue == ENQUEUED)) { return false; } assert queue == this; // Self-loop end, so if a FinalReference it remains inactive. // 如果連結串列沒有元素,則此引用例項直接作為頭節點,否則把前一個引用例項作為下一個節點 r.next = (head == null) ? r : head; // 當前例項更新為頭節點,也就是每一個新入隊的引用例項都是作為頭節點,已有的引用例項會作為後繼節點 head = r; // 佇列長度增加1 queueLength++; // Update r.queue *after* adding to list, to avoid race // with concurrent enqueued checks and fast-path poll(). // Volatiles ensure ordering. // 當前引用例項已經入隊,那麼它本身持有的引用佇列例項置為ReferenceQueue.ENQUEUED r.queue = ENQUEUED; // 特殊處理FinalReference,VM進行計數 if (r instanceof FinalReference) { VM.addFinalRefCount(1); } // 喚醒所有等待的執行緒 lock.notifyAll(); return true; } } // 引用佇列的poll操作,此方法必須在加鎖情況下呼叫 private Reference<? extends T> reallyPoll() { Reference<? extends T> r = head; if (r != null) { r.queue = NULL; // Update r.queue *before* removing from list, to avoid // race with concurrent enqueued checks and fast-path // poll(). Volatiles ensure ordering. @SuppressWarnings("unchecked") Reference<? extends T> rn = r.next; // Handle self-looped next as end of list designator. // 更新next節點為頭節點,如果next節點為自身,那麼佇列中只有當前這個物件一個元素 head = (rn == r) ? null : rn; // Self-loop next rather than setting to null, so if a // FinalReference it remains inactive. // 當前頭節點變更為環狀佇列,考慮到FinalReference尚為inactive和避免重複出隊的問題 r.next = r; // 佇列長度減少1 queueLength--; // 特殊處理FinalReference,VM進行計數 if (r instanceof FinalReference) { VM.addFinalRefCount(-1); } return r; } return null; } // 佇列的公有poll操作,主要是加鎖後呼叫reallyPoll public Reference<? extends T> poll() { if (head == null) return null; synchronized (lock) { return reallyPoll(); } } // ... }
從原始碼上看,實際上ReferenceQueue
只是名義上的引用佇列,它只儲存了Reference
連結串列的頭(head)節點,並且提供了出隊、入隊等操作,而Reference
實際上本身提供單向連結串列的功能,也就是Reference
通過屬性next構建單向連結串列,而連結串列的操作通過ReferenceQueue這個類來
完成。
相關文章的連結如下:
1、在Ubuntu 16.04上編譯OpenJDK8的原始碼
13、類載入器
14、類的雙親委派機制
15、核心類的預裝載
16、Java主類的裝載
17、觸發類的裝載
18、類檔案介紹
19、檔案流
20、解析Class檔案
21、常量池解析(1)
22、常量池解析(2)
23、欄位解析(1)
24、欄位解析之偽共享(2)
25、欄位解析(3)
28、方法解析
29、klassVtable與klassItable類的介紹
30、計算vtable的大小
31、計算itable的大小
32、解析Class檔案之建立InstanceKlass物件
33、欄位解析之欄位注入
34、類的連線
35、類的連線之驗證
36、類的連線之重寫(1)
37、類的連線之重寫(2)
38、方法的連線
39、初始化vtable
40、初始化itable
41、類的初始化
作者持續維護的個人部落格classloading.com。
關注公眾號,有HotSpot原始碼剖析系列文章!