若是隻提供強引用,咱們就很難寫出「這個對象不是很重要,若是內存不足GC回收掉也是能夠的」這種語義的代碼。Java在1.2版本中完善了引用體系,提供了4中引用類型:強引用,軟引用,弱引用,虛引用。使用這些引用類型,咱們不但能夠控制垃圾回收器對對象的回收策略,同時還能在對象被回收後獲得通知,進行相應的後續操做。java
引用與可達性分類算法
Java目前有4中引用類型:sql
相較於傳統的引用計數算法,Java使用可達性分析來判斷一個對象是否存活。其基本思路是從GC Root開始向下搜索,若是對象與GC Root之間存在引用鏈,則對象是可達的。對象的可達性與引用類型密切相關。Java有5中類型的可達性:緩存
對象的引用類型與可達性聽着有點亂,好像是一回事,咱們這裏實例分析一下:數據結構
上面這個例子中,A~D,每一個對象只存在一個引用,分別是:A-強引用,B-軟引用,C-弱引用,D-虛引用,因此他們的可達性爲:A-強可達,B-軟可達,C-弱可達,D-虛可達。由於E沒有存在和GC Root的引用鏈,因此它是不可達。多線程
在看一個複雜的例子:架構
同時能夠看出,對象的可達性是會發生變化的,隨着運行時引用對象的引用類型的變化,可達性也會發生變化,能夠參考下圖:併發
Reference整體結構分佈式
Reference類是全部引用類型的基類,Java提供了具體引用類型的具體實現:函數
由於默認的引用就是強引用,因此沒有強引用的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及其子類有兩大功能:
第一個功能已經解釋過了,第二個功能是如何作到的呢?
一種思路是在新建一個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中狀態:
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的完整流程以下:
歡迎工做一到五年的Java工程師朋友們加入Java架構開發 : 867748702 羣內提供免費的Java架構學習資料(裏面有高可用、高併發、高性能及分佈式、 Jvm性能調優、Spring源碼,MyBatis,Netty,Redis,Kafka,Mysql,Zookeeper, Tomcat,Docker,Dubbo,Nginx等多個知識點的架構資料) 合理利用本身每一分每一秒的時間來學習提高本身, 不要再用"沒有時間「來掩飾本身思想上的懶惰!趁年輕,使勁拼,給將來的本身一個交代!