JDK 源碼閱讀 Reference

來源:互聯網
上載者:User

標籤:int   extend   垃圾   擴充   end   dex   vat   null   指標   

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<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中狀態:

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<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

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.