JDK 源碼閱讀 Reference

Java最初只有普通的強引用,只有對象存在引用,則對象就不會被回收,即便內存不足,也是如此,JVM會爆出OOME,也不會去回收存在引用的對象。

若是隻提供強引用,咱們就很難寫出「這個對象不是很重要,若是內存不足GC回收掉也是能夠的」這種語義的代碼。Java在1.2版本中完善了引用體系,提供了4中引用類型:強引用,軟引用,弱引用,虛引用。使用這些引用類型,咱們不但能夠控制垃圾回收器對對象的回收策略,同時還能在對象被回收後獲得通知,進行相應的後續操做。java

引用與可達性分類算法

Java目前有4中引用類型:sql

  1. 強引用(Strong Reference):普通的的引用類型,new一個對象默認獲得的引用就是強引用,只要對象存在強引用,就不會被GC。
  2. 軟引用(Soft Reference):相對較弱的引用,垃圾回收器會在內存不足時回收弱引用指向的對象。JVM會在拋出OOME前清理全部弱引用指向的對象,若是清理完仍是內存不足,纔會拋出OOME。因此軟引用通常用於實現內存敏感緩存。
  3. 弱引用(Weak Reference):更弱的引用類型,垃圾回收器在GC時會回收此對象,也能夠用於實現緩存,好比JDK提供的WeakHashMap。
  4. 虛引用(Phantom Reference):一種特殊的引用類型,不能經過虛引用獲取到關聯對象,只是用於獲取對象被回收的通知。

相較於傳統的引用計數算法,Java使用可達性分析來判斷一個對象是否存活。其基本思路是從GC Root開始向下搜索,若是對象與GC Root之間存在引用鏈,則對象是可達的。對象的可達性與引用類型密切相關。Java有5中類型的可達性:緩存

  1. 強可達(Strongly Reachable):若是線程能經過強引用訪問到對象,那麼這個對象就是強可達的。
  2. 軟可達(Soft Reachable):若是一個對象不是強可達的,可是能夠經過軟引用訪問到,那麼這個對象就是軟可達的
  3. 弱可達(Weak Reachable):若是一個對象不是強可達或者軟可達的,可是能夠經過弱引用訪問到,那麼這個對象就是弱可達的。
  4. 虛可達(Phantom Reachable):若是一個對象不是強可達,軟可達或者弱可達,而且這個對象已經finalize過了,而且有虛引用指向該對象,那麼這個對象就是虛可達的。
  5. 不可達(Unreachable):若是對象不能經過上述的幾種方式訪問到,則對象是不可達的,能夠被回收。

對象的引用類型與可達性聽着有點亂,好像是一回事,咱們這裏實例分析一下:數據結構

JDK 源碼閱讀 Reference

 

上面這個例子中,A~D,每一個對象只存在一個引用,分別是:A-強引用,B-軟引用,C-弱引用,D-虛引用,因此他們的可達性爲:A-強可達,B-軟可達,C-弱可達,D-虛可達。由於E沒有存在和GC Root的引用鏈,因此它是不可達。多線程

在看一個複雜的例子:架構

JDK 源碼閱讀 Reference

 

  • A依然只有一個強引用,因此A是強可達
  • B存在兩個引用,強引用和軟引用,可是B能夠經過強引用訪問到,因此B是強可達
  • C只能經過弱引用訪問到,因此是弱可達
  • D存在弱引用和虛引用,因此是弱可達
  • E雖然存在F的強引用,可是GC Root沒法訪問到它,因此它依然是不可達。

同時能夠看出,對象的可達性是會發生變化的,隨着運行時引用對象的引用類型的變化,可達性也會發生變化,能夠參考下圖:併發

JDK 源碼閱讀 Reference

 

Reference整體結構分佈式

Reference類是全部引用類型的基類,Java提供了具體引用類型的具體實現:函數

JDK 源碼閱讀 Reference

 

  • SoftReference:軟引用,堆內存不足時,垃圾回收器會回收對應引用
  • WeakReference:弱引用,每次垃圾回收都會回收其引用
  • PhantomReference:虛引用,對引用無影響,只用於獲取對象被回收的通知
  • FinalReference:Java用於實現finalization的一個內部類

由於默認的引用就是強引用,因此沒有強引用的Reference實現類。

Reference的核心

Java的多種引用類型實現,不是經過擴展語法實現的,而是利用類實現的,Reference類表示一個引用,其核心代碼就是一個成員變量reference:

 

public abstract class Reference<T> {
 private T referent; // 會被GC特殊對待
 
 // 獲取Reference管理的對象
 public T get() {
 return this.referent;
 }
 
 // ...
}

若是JVM沒有對這個變量作特殊處理,它依然只是一個普通的強引用,之因此會出現不一樣的引用類型,是由於JVM垃圾回收器硬編碼識別SoftReference,WeakReference,PhantomReference等這些具體的類,對其reference變量進行特殊對象,纔有了不一樣的引用類型的效果。

上文提到了Reference及其子類有兩大功能:

  1. 實現特定的引用類型
  2. 用戶能夠對象被回收後獲得通知

第一個功能已經解釋過了,第二個功能是如何作到的呢?

一種思路是在新建一個Reference實例是,添加一個回調,當java.lang.ref.Reference#referent被回收時,JVM調用該回調,這種思路比較符合通常的通知模型,可是對於引用與垃圾回收這種底層場景來講,會致使實現複雜,性能不高的問題,好比須要考慮在什麼線程中執行這個回調,回調執行阻塞怎麼辦等等。

因此Reference使用了一種更加原始的方式來作通知,就是把引用對象被回收的Reference添加到一個隊列中,用戶後續本身去從隊列中獲取並使用。

理解了設計後對應到代碼上就好理解了,Reference有一個queue成員變量,用於存儲引用對象被回收的Reference實例:

 

public abstract class Reference<T> {
 // 會被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中狀態:

  1. Active:新建立的實例的狀態,由垃圾回收器進行處理,若是實例的可達性處於合適的狀態,垃圾回收器會切換實例的狀態爲Pending或者Inactive。若是Reference註冊了ReferenceQueue,則會切換爲Pending,而且Reference會加入pending-Reference鏈表中,若是沒有註冊ReferenceQueue,會切換爲Inactive。
  2. Pending:在pending-Reference鏈表中的Reference的狀態,這些Reference等待被加入ReferenceQueue中。
  3. Enqueued:在ReferenceQueue隊列中的Reference的狀態,若是Reference從隊列中移除,會進入Inactive狀態
  4. Inactive:Reference的最終狀態

Reference對象圖以下:

JDK 源碼閱讀 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<T> {
 // 會被GC特殊對待
 private T referent; 
 // reference被回收後,當前Reference實例會被添加到這個隊列中
 volatile ReferenceQueue<? super T> queue; 
 
 // 全局惟一的pending-Reference列表
 private static Reference<Object> pending = null;
 
 // Reference爲Active:由垃圾回收器管理的已發現的引用列表(這個不在本文討論訪問內)
 // Reference爲Pending:在pending列表中的下一個元素,若是沒有爲null
 // 其餘狀態:NULL
 transient private Reference<T> 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<Object> r;
 // 獲取鎖,避免與垃圾回收器同時操做
 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<Object> q = r.queue;
 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<T> {
 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<T> {
 /* When active: NULL
 * pending: this
 * Enqueued: 指向ReferenceQueue中的下一個元素,若是沒有,指向this
 * Inactive: this
 */
 Reference next;
 
 // ...
}

總結

一個使用Reference+ReferenceQueue的完整流程以下:

JDK 源碼閱讀 Reference

歡迎工做一到五年的Java工程師朋友們加入Java架構開發 : 867748702 羣內提供免費的Java架構學習資料(裏面有高可用、高併發、高性能及分佈式、 Jvm性能調優、Spring源碼,MyBatis,Netty,Redis,Kafka,Mysql,Zookeeper, Tomcat,Docker,Dubbo,Nginx等多個知識點的架構資料) 合理利用本身每一分每一秒的時間來學習提高本身, 不要再用"沒有時間「來掩飾本身思想上的懶惰!趁年輕,使勁拼,給將來的本身一個交代!

相關文章
相關標籤/搜索