引用的分類
Java 1.2以后,除了普通的引用外,Java還定義了軟引用、弱引用、虛引用等概念。
- 強引用:GC root引用
- 軟引用(Soft Reference):通過
java.lang.ref.SoftReference
引用的對象,可以通過get
操作獲取所引用的對象,所引用對象會延遲到在即將OOM時回收 - 弱引用(Weak Reference):通過
java.lang.ref.WeakReference
引用的對象,可以通過get
操作獲取所引用的對象,不會影響垃圾收集器的行為,所引用對象會在下次垃圾收集時回收 - 虛引用(Phantom Reference):通過
java.lang.ref.PhantomReference
引用的對象,不能通過get
操作獲取所引用對象(無論何時都會返回null),不會影響垃圾收集器的行為,會在下次垃圾收集時回收。在PhantomReference實例時,必須要傳入一個ReferenceQueue實例用于實現通知。
JDK中的引用(Reference)
Java使用java.lang.ref
下的類表示和管理對象的引用狀態,如上面提到的三種其他引用,以及finalization在Java語言層上的實現。通過這些類與JVM進行交互,共同實現Java對這些引用的邏輯。除了強引用外,Java通過java.lang.ref.Reference<T>
實現其他類型的引用。Reference中定義了引用的狀態(State),當發生一次GC后,某些引用的狀態會隨之發生改變。狀態改變后,某些引用可以通過放置到用戶指定的java.lang.ref.ReferenceQueue
實例,實現被引用的對象失效后對引用實例本身的操作,比如在引用失效后通知給用戶。
Java所有的除了強引用之外的引用都通過java.lang.ref.Reference<T>
抽象類實現,該類某些邏輯是通過與JVM的操作緊密結合而實現的,所以除了java.lang.ref
下繼承它的子類可以被JVM識別,自己繼承這個抽象類是沒有任何意義的。Reference通過一個referent
的泛型引用保存被引用的對象,同時也持有一個queue引用保存一個ReferenceQueue<? super T>
的實例用于對引用的注冊(register)操作,用于在被引用對象失效后將引用注冊進隊列。Reference本身也被實現成一個鏈表,當一個Reference作為一個引用時,其next為null,如果作為一個pending引用鏈出現,next要么是this(鏈表尾),要么是其它的引用實例。
引用(Reference)的狀態
Reference將引用的狀態分為有效(Active)、掛起(Pending)、待處理(Enqueued)、不可用(Inactive)。通過判斷一個Reference實例是否被注冊(is registered)到該Reference實例來影響一個引用被GC后的狀態變化。
- 有效(Active):新創建的引用實例,在其被引用的對象被回收之前是有效的
- 掛起(Pending):其被引用的對象被回收之后被放到pending-Reference列表中,等待Reference-handler線程處理的引用
- 待處理(Enqueued):一個在ReferenceQueue隊列實例中的引用
- 無效(Inactive):不再可用的引用。
只有在一個被注冊(包含一個ReferenceQueue實例引用)的引用中可能存在掛起(Pending)和待處理(Enqueued)狀態。
引用(Reference)的生命周期
一個引用的生命周期通常是這樣子的:
首先,當一個引用被創建時,無論有沒有被注冊,總是有效的(Active)。在GC標記階段,如果一個referent被標記為不可達(沒有GC root),收集器在檢測到referent的可達性發生變化(由可達變為不可達)后,如果一個引用是被注冊的,那么JVM會將該引用更改為掛起(Pending)狀態,否則直接不可用(Inactive)。
怎么判斷一個引用是否被注冊呢?通過這個引用是否有持有一個非Null的ReferenceQueue實例。如果用戶沒有在構造引用實例時手動傳入一個ReferenceQueue,那么這個引用就是未被注冊的。這個Null也是一個類,是一個ReferenceQueue內部狀態類,沒有別的作用,僅僅作為一個生成空對象的實例使用。
private static class Null extends ReferenceQueue {
boolean enqueue(Reference r) {
return false;
}
}
若一個引用被注冊,那么JVM會將該引用實例添加到pending-Reference列表中,并修改其next,該引用正式處于掛起(Pending)狀態。所謂的pending-Reference列表,就是Reference中的一個特殊的私有靜態引用,“添加到pending-Reference列表中”其實就是一個賦值(set)操作。當一個引用被掛起(Pending)后,唯一的目的就是等待Reference-handler線程將其從pending-Reference列表中移動到ReferenceQueue。
Reference-handler線程將掛起的引用從pending-Reference列表中移動到它被注冊的ReferenceQueue后,這個引用的狀態就成了待處理(Enqueued)。由于ReferenceQueue是用戶指定的,所以用戶可以對這個狀態的引用進行操作,也可以說,被注冊的引用在被GC后,用戶可以得到一個通知。ReferenceQueue也可以說是一個消息隊列,用戶可以對里面的引用進行操作,典型的應用就是WeakHashMap對弱引用的處理。一旦里面的引用被移出隊列,那么該引用的狀態就會變為最終態——無效(Inactive)狀態。無效狀態的引用再也不會更改為其他狀態,只能等待自身被GC。
再談pending-Reference列表
pending-Reference列表,就是Reference中的一個特殊的私有靜態引用:
private static Reference pending = null;
與之類似的還有discovered
transient private Reference<T> discovered;
為什么說這兩個變量特殊,是因為Java中沒有任何對該引用賦值的定義,那么如何將引用實例放入pending字段中呢?這由VM對字節碼的調用完成。openjdk中的hotspot源碼中,hotspot/src/share/vm/memory/referenceProcessor.cpp
這個文件中有一個ReferenceProcessor::discover_reference
方法,根據此方法的注釋由了解到虛擬機在對Reference的處理有ReferenceBasedDiscovery和RefeferentBasedDiscovery兩種策略。這兩個策略的實現不在討論范圍內,此處省略不提。總之,VM通過對Reference的操作,實現了引用狀態的變更,由于這些類都是在java.lang
下,所以這也是用戶手動繼承實現一個引用類可能會無效的原因了。
Reference-handler線程
在Reference內部有一個類叫ReferenceHandler
,它繼承了Thread,是Reference-handler的實現。這個類主要的作用就是將pending中的鏈表節點逐個移動到Reference實例的ReferenceQueue中,最終將pending還原為null,如果pending為null,這個線程將會無限期掛起。
這個類是在Reference的靜態代碼塊中實例化并運行的,由類加載的知識可以知道類的初始化在第一次使用這個類的時候在其之前完成,所以當用戶決定使用一個Reference子類時,就會開始這個線程。線程默認的優先級就是最高的優先級MAX_PRIORITY
,如果某個系統擁有比MAX_PRIORITY
還要高得等級,該線程也會和內核線程同等優先級運行。總之,開始這個線程之前,Reference會保證這個線程以最高優先級運行。同時,這也是一個守護線程。
如果用戶線程已經全部退出運行了,只剩下守護線程存在了,那么虛擬機也會退出,即退出程序。 因為沒有了被守護者,守護線程也就沒有工作可做了,也就沒有繼續運行程序的必要了。
- thread.setDaemon(true)必須在thread.start()之前設置,否則會跑出一個IllegalThreadStateException異常。你不能把正在運行的常規線程設置為守護線程。
- 在Daemon線程中產生的新線程也是Daemon的。
- 守護線程應該永遠不去訪問固有資源,如文件、數據庫,因為它會在任何時候甚至在一個操作的中間發生中斷。
enqueued 操作
Reference-handler線程的enqueued操作是通過調用Reference的ReferenceQueue實現的。本質就是調用ReferenceQueue的enqueued方法,傳入需要enqueued的引用。enqueued方法將該引用的狀態更改為ENQUEUED
,此時這個引用的ReferenceQueue被替換成Null類,然后使用頭插法把這個引用插入這個隊列的隊頭里。如果已經是ENQUEUED
狀態的引用會直接退出方法。
這個方法,如果enqueued操作成功,即成功將一個引用插入隊列,則返回true,其他情況返回false。
enqueued操作會鎖定傳入的引用對象,所以是同步的,而且入隊時會進一步鎖定隊列,防止并發情況下插入失敗。
引用鎖
上文提到,如果pending為null,Reference-handler線程將會無限期掛起。那么總是要喚醒這個線程的,在哪里喚醒這個線程呢?要聊到這個話題,就要聊到Reference的鎖。java.lang.ref.Reference<T>
和java.lang.ref.ReferenceQueue<T>
中,各有一個自定義的鎖類,上文提到的對象狀態變更需要的同步操作,都需要持有這兩個鎖類的鎖才能完成。兩個類對鎖的定義都很簡單,就是一個空的類。
java.lang.ref.Reference<T>
的鎖
static private class Lock { };
private static Lock lock = new Lock();
java.lang.ref.ReferenceQueue<T>
的鎖
static private class Lock { };
private Lock lock = new Lock();
唯一的區別就是Reference的鎖類引用帶有static,帶有static是因為對象用于與垃圾收集器同步。收集器必須在每個收集周期的開始處獲取此鎖。因此任何持有此鎖的代碼盡可能快地完成,不應該在持有這個鎖的時候分配新對象,而且應避免調用用戶代碼。
可是代碼中并沒有Reference中鎖的任何類似調用nolify方法等的喚醒操作,所以筆者認為,喚醒操作應該也是在JVM內部實現的。至于時機,可能是當一次GC結束后。
而ReferenceQueue的鎖相對簡單。當某個線程執行remove操作時,如果是空隊列,則掛起這個線程,僅當達到Timeout或者執行enqueue操作才會被喚醒。由于只通過引用類調用,所以只有當狀態更改時才會喚醒。Finalize線程會調用remove方法,這里不再詳述。
Finalizer和FinalReference
finalize的執行也大同小異,都是通過static語句塊啟動一個線程,只是這里啟動的是低優先級的線程。,而且最終的調用邏輯是通過sun.misc.JavaLangAccess
類完成的。當然Runtime.runFinalization()
方法和java.lang.Shutdown
類通過調用native方法,再通過native中回調Finalizer中的runAllFinalizers方法也能執行finalize的調用。至于finalize是如何調用的,網上有博客,我就不再贅述了。