(點選上方公眾號,可快速關註)
來源:木杉的部落格 ,
imushan.com/2018/08/19/java/language/JDK原始碼閱讀-Reference/
Java最初只有普通的強取用,只有物件存在取用,則物件就不會被回收,即使記憶體不足,也是如此,JVM會爆出OOME,也不會去回收存在取用的物件。
如果只提供強取用,我們就很難寫出“這個物件不是很重要,如果記憶體不足GC回收掉也是可以的”這種語意的程式碼。Java在1.2版本中完善了取用體系,提供了4中取用型別:強取用,軟取用,弱取用,虛取用。使用這些取用型別,我們不但可以控制垃圾回收器對物件的回收策略,同時還能在物件被回收後得到通知,進行相應的後續操作。
取用與可達性分類
Java目前有4中取用型別:
-
強取用(Strong Reference):普通的的取用型別,new一個物件預設得到的取用就是強取用,只要物件存在強取用,就不會被GC。
-
軟取用(Soft Reference):相對較弱的取用,垃圾回收器會在記憶體不足時回收弱取用指向的物件。JVM會在丟擲OOME前清理所有弱取用指向的物件,如果清理完還是記憶體不足,才會丟擲OOME。所以軟取用一般用於實現記憶體敏感快取。
-
弱取用(Weak Reference):更弱的取用型別,垃圾回收器在GC時會回收此物件,也可以用於實現快取,比如JDK提供的WeakHashMap。
-
虛取用(Phantom Reference):一種特殊的取用型別,不能透過虛取用獲取到關聯物件,只是用於獲取物件被回收的通知。
相較於傳統的取用計數演演算法,Java使用可達性分析來判斷一個物件是否存活。其基本思路是從GC Root開始向下搜尋,如果物件與GC Root之間存在取用鏈,則物件是可達的。物件的可達性與取用型別密切相關。Java有5中型別的可達性:
-
強可達(Strongly Reachable):如果執行緒能透過強取用訪問到物件,那麼這個物件就是強可達的。
-
軟可達(Soft Reachable):如果一個物件不是強可達的,但是可以透過軟取用訪問到,那麼這個物件就是軟可達的
-
弱可達(Weak Reachable):如果一個物件不是強可達或者軟可達的,但是可以透過弱取用訪問到,那麼這個物件就是弱可達的。
-
虛可達(Phantom Reachable):如果一個物件不是強可達,軟可達或者弱可達,並且這個物件已經finalize過了,並且有虛取用指向該物件,那麼這個物件就是虛可達的。
-
不可達(Unreachable):如果物件不能透過上述的幾種方式訪問到,則物件是不可達的,可以被回收。
物件的取用型別與可達性聽著有點亂,好像是一回事,我們這裡實體分析一下:
上面這個例子中,A~D,每個物件只存在一個取用,分別是:A-強取用,B-軟取用,C-弱取用,D-虛取用,所以他們的可達性為:A-強可達,B-軟可達,C-弱可達,D-虛可達。因為E沒有存在和GC Root的取用鏈,所以它是不可達。
在看一個複雜的例子:
-
A依然只有一個強取用,所以A是強可達
-
B存在兩個取用,強取用和軟取用,但是B可以透過強取用訪問到,所以B是強可達
-
C只能透過弱取用訪問到,所以是弱可達
-
D存在弱取用和虛取用,所以是弱可達
-
E雖然存在F的強取用,但是GC Root無法訪問到它,所以它依然是不可達。
同時可以看出,物件的可達性是會發生變化的,隨著執行時取用物件的取用型別的變化,可達性也會發生變化,可以參考下圖:
Reference總體結構
Reference類是所有取用型別的基類,Java提供了具體取用型別的具體實現:
-
SoftReference:軟取用,堆記憶體不足時,垃圾回收器會回收對應取用
-
WeakReference:弱取用,每次垃圾回收都會回收其取用
-
PhantomReference:虛取用,對取用無影響,只用於獲取物件被回收的通知
-
FinalReference:Java用於實現finalization的一個內部類
因為預設的取用就是強取用,所以沒有強取用的Reference實現類。
Reference的核心
Java的多種取用型別實現,不是透過擴充套件語法實現的,而是利用類實現的,Reference類表示一個取用,其核心程式碼就是一個成員變數reference:
public abstract class Reference
{ private T referent; // 會被GC特殊對待
// 獲取Reference管理的物件
public T get() {
return this.referent;
}
// …
}
如果JVM沒有對這個變數做特殊處理,它依然只是一個普通的強取用,之所以會出現不同的取用型別,是因為JVM垃圾回收器硬編碼識別SoftReference,WeakReference,PhantomReference等這些具體的類,對其reference變數進行特殊物件,才有了不同的取用型別的效果。
上文提到了Reference及其子類有兩大功能:
-
實現特定的取用型別
-
使用者可以物件被回收後得到通知
第一個功能已經解釋過了,第二個功能是如何做到的呢?
一種思路是在新建一個Reference實體是,新增一個回呼,當java.lang.ref.Reference#referent被回收時,JVM呼叫該回呼,這種思路比較符合一般的通知模型,但是對於取用與垃圾回收這種底層場景來說,會導致實現複雜,效能不高的問題,比如需要考慮在什麼執行緒中執行這個回呼,回呼執行阻塞怎麼辦等等。
所以Reference使用了一種更加原始的方式來做通知,就是把取用物件被回收的Reference新增到一個佇列中,使用者後續自己去從佇列中獲取並使用。
理解了設計後對應到程式碼上就好理解了,Reference有一個queue成員變數,用於儲存取用物件被回收的Reference實體:
public abstract class Reference
{ // 會被GC特殊對待
private T referent;
// reference被回收後,當前Reference實體會被新增到這個佇列中
volatile ReferenceQueue super T> queue;
// 只傳入reference的建構式,意味著使用者只需要特殊的取用型別,不關心物件何時被GC
Reference(T referent) {
this(referent, null);
}
// 傳入referent和ReferenceQueue的建構式,reference被回收後,會新增到queue中
Reference(T referent, ReferenceQueue super T> queue) {
this.referent = referent;
this.queue = (queue == null) ? ReferenceQueue.NULL : queue;
}
// …
}
Reference的狀態
Reference物件是有狀態的。一共有4中狀態:
-
Active:新建立的實體的狀態,由垃圾回收器進行處理,如果實體的可達性處於合適的狀態,垃圾回收器會切換實體的狀態為Pending或者Inactive。如果Reference註冊了ReferenceQueue,則會切換為Pending,並且Reference會加入pending-Reference連結串列中,如果沒有註冊ReferenceQueue,會切換為Inactive。
-
Pending:在pending-Reference連結串列中的Reference的狀態,這些Reference等待被加入ReferenceQueue中。
-
Enqueued:在ReferenceQueue佇列中的Reference的狀態,如果Reference從佇列中移除,會進入Inactive狀態
-
Inactive:Reference的最終狀態
Reference物件圖如下:
除了上文提到的ReferenceQueue,這裡出現了一個新的資料結構:pending-Reference。這個連結串列是用來乾什麼的呢?
上文提到了,reference取用的物件被回收後,該Reference實體會被新增到ReferenceQueue中,但是這個不是垃圾回收器來做的,這個操作還是有一定邏輯的,如果垃圾回收器還需要執行這個操作,會降低其效率。從另外一方面想,Reference實體會被新增到ReferenceQueue中的實效性要求不高,所以也沒必要在回收時立馬加入ReferenceQueue。
所以垃圾回收器做的是一個更輕量級的操作:把Reference新增到pending-Reference連結串列中。Reference物件中有一個pending成員變數,是靜態變數,它就是這個pending-Reference連結串列的頭結點。要組成連結串列,還需要一個指標,指向下一個節點,這個對應的是java.lang.ref.Reference#discovered這個成員變數。
可以看一下程式碼:
public abstract class Reference
{ // 會被GC特殊對待
private T referent;
// reference被回收後,當前Reference實體會被新增到這個佇列中
volatile ReferenceQueue super T> queue;
// 全域性唯一的pending-Reference串列
private static Reference
// Reference為Active:由垃圾回收器管理的已發現的取用串列(這個不在本文討論訪問內)
// Reference為Pending:在pending串列中的下一個元素,如果沒有為null
// 其他狀態:NULL
transient private Reference
discovered; /* used by VM */ // …
}
ReferenceHandler執行緒
透過上文的討論,我們知道一個Reference實體化後狀態為Active,其取用的物件被回收後,垃圾回收器將其加入到pending-Reference連結串列,等待加入ReferenceQueue。這個過程是如何實現的呢?
這個過程不能對垃圾回收器產生影響,所以不能在垃圾回收執行緒中執行,也就需要一個獨立的執行緒來負責。這個執行緒就是ReferenceHandler,它定義在Reference類中:
// 用於控制垃圾回收器操作與Pending狀態的Reference入隊操作不衝突執行的全域性鎖
// 垃圾回收器開始一輪垃圾回收前要獲取此鎖
// 所以所有佔用這個鎖的程式碼必須儘快完成,不能生成新物件,也不能呼叫使用者程式碼
static private class Lock { };
private static Lock lock = new Lock();
private static class ReferenceHandler extends Thread {
ReferenceHandler(ThreadGroup g, String name) {
super(g, name);
}
public void run() {
// 這個執行緒一直執行
for (;;) {
Reference
// 獲取鎖,避免與垃圾回收器同時操作
synchronized (lock) {
// 判斷pending-Reference連結串列是否有資料
if (pending != null) {
// 如果有Pending Reference,從串列中取出
r = pending;
pending = r.discovered;
r.discovered = null;
} else {
// 如果沒有Pending Reference,呼叫wait等待
//
// wait等待鎖,是可能丟擲OOME的,
// 因為可能發生InterruptedException異常,然後就需要實體化這個異常物件,
// 如果此時記憶體不足,就可能丟擲OOME,所以這裡需要捕獲OutOfMemoryError,
// 避免因為OOME而導致ReferenceHandler行程靜默退出
try {
try {
lock.wait();
} catch (OutOfMemoryError x) { }
} catch (InterruptedException x) { }
continue;
}
}
// 如果Reference是Cleaner,呼叫其clean方法
// 這與Cleaner機制有關係,不在此文的討論訪問
if (r instanceof Cleaner) {
((Cleaner)r).clean();
continue;
}
// 把Reference新增到關聯的ReferenceQueue中
// 如果Reference構造時沒有關聯ReferenceQueue,會關聯ReferenceQueue.NULL,這裡就不會進行入隊操作了
ReferenceQueue
if (q != ReferenceQueue.NULL) q.enqueue(r);
}
}
}
ReferenceHandler執行緒是在Reference的static塊中啟動的:
static {
// 獲取system ThreadGroup
ThreadGroup tg = Thread.currentThread().getThreadGroup();
for (ThreadGroup tgn = tg;
tgn != null;
tg = tgn, tgn = tg.getParent());
Thread handler = new ReferenceHandler(tg, “Reference Handler”);
// ReferenceHandler執行緒有最高優先順序
handler.setPriority(Thread.MAX_PRIORITY);
handler.setDaemon(true);
handler.start();
}
綜上,ReferenceHandler是一個最高優先順序的執行緒,其邏輯是從Pending-Reference連結串列中取出Reference,新增到其關聯的Reference-Queue中。
ReferenceQueue
Reference-Queue也是一個連結串列:
public class ReferenceQueue
{ private volatile Reference extends T> head = null;
// …
}
// ReferenceQueue中的這個鎖用於保護連結串列佇列在多執行緒環境下的正確性
static private class Lock { };
private Lock lock = new Lock();
boolean enqueue(Reference extends T> r) { /* Called only by Reference class */
synchronized (lock) {
// 判斷Reference是否需要入隊
ReferenceQueue > queue = r.queue;
if ((queue == NULL) || (queue == ENQUEUED)) {
return false;
}
assert queue == this;
// Reference入隊後,其queue變數設定為ENQUEUED
r.queue = ENQUEUED;
// Reference的next變數指向ReferenceQueue中下一個元素
r.next = (head == null) ? r : head;
head = r;
queueLength++;
if (r instanceof FinalReference) {
sun.misc.VM.addFinalRefCount(1);
}
lock.notifyAll();
return true;
}
}
透過上面的程式碼,可以知道java.lang.ref.Reference#next的用途了:
public abstract class Reference
{ /* When active: NULL
* pending: this
* Enqueued: 指向ReferenceQueue中的下一個元素,如果沒有,指向this
* Inactive: this
*/
Reference next;
// …
}
總結
一個使用Reference+ReferenceQueue的完整流程如下:
參考資料
-
Java Reference詳解 – robin-yao的個人頁面 – 開源中國
https://my.oschina.net/robinyao/blog/829983
-
Internals of Java Reference Object
http://www.javarticles.com/2016/10/internals-of-java-reference-object.html
-
java.lang.ref (Java Platform SE 7 )
https://docs.oracle.com/javase/7/docs/api/java/lang/ref/package-summary.html#reachability
-
Java Reference Objects
http://www.kdgregory.com/index.php?page=java.refobj
-
Java核心技術36講 第4講
【關於投稿】
如果大家有原創好文投稿,請直接給公號傳送留言。
① 留言格式:
【投稿】+《 文章標題》+ 文章連結
② 示例:
【投稿】《不要自稱是程式員,我十多年的 IT 職場總結》:http://blog.jobbole.com/94148/
③ 最後請附上您的個人簡介哈~
看完本文有收穫?請轉發分享給更多人
關註「ImportNew」,提升Java技能