Java實(shí)現(xiàn)的引用

引用的分類

Java 1.2以后,除了普通的引用外,Java還定義了軟引用、弱引用、虛引用等概念。

  • 強(qiáng)引用:GC root引用
  • 軟引用(Soft Reference):通過java.lang.ref.SoftReference引用的對(duì)象,可以通過get操作獲取所引用的對(duì)象,所引用對(duì)象會(huì)延遲到在即將OOM時(shí)回收
  • 弱引用(Weak Reference):通過java.lang.ref.WeakReference引用的對(duì)象,可以通過get操作獲取所引用的對(duì)象,不會(huì)影響垃圾收集器的行為,所引用對(duì)象會(huì)在下次垃圾收集時(shí)回收
  • 虛引用(Phantom Reference):通過java.lang.ref.PhantomReference引用的對(duì)象,不能通過get操作獲取所引用對(duì)象(無論何時(shí)都會(huì)返回null),不會(huì)影響垃圾收集器的行為,會(huì)在下次垃圾收集時(shí)回收。在PhantomReference實(shí)例時(shí),必須要傳入一個(gè)ReferenceQueue實(shí)例用于實(shí)現(xiàn)通知。

JDK中的引用(Reference)

Java使用java.lang.ref下的類表示和管理對(duì)象的引用狀態(tài),如上面提到的三種其他引用,以及finalization在Java語言層上的實(shí)現(xiàn)。通過這些類與JVM進(jìn)行交互,共同實(shí)現(xiàn)Java對(duì)這些引用的邏輯。除了強(qiáng)引用外,Java通過java.lang.ref.Reference<T>實(shí)現(xiàn)其他類型的引用。Reference中定義了引用的狀態(tài)(State),當(dāng)發(fā)生一次GC后,某些引用的狀態(tài)會(huì)隨之發(fā)生改變。狀態(tài)改變后,某些引用可以通過放置到用戶指定的java.lang.ref.ReferenceQueue實(shí)例,實(shí)現(xiàn)被引用的對(duì)象失效后對(duì)引用實(shí)例本身的操作,比如在引用失效后通知給用戶。

Java所有的除了強(qiáng)引用之外的引用都通過java.lang.ref.Reference<T>抽象類實(shí)現(xiàn),該類某些邏輯是通過與JVM的操作緊密結(jié)合而實(shí)現(xiàn)的,所以除了java.lang.ref下繼承它的子類可以被JVM識(shí)別,自己繼承這個(gè)抽象類是沒有任何意義的。Reference通過一個(gè)referent的泛型引用保存被引用的對(duì)象,同時(shí)也持有一個(gè)queue引用保存一個(gè)ReferenceQueue<? super T>的實(shí)例用于對(duì)引用的注冊(cè)(register)操作,用于在被引用對(duì)象失效后將引用注冊(cè)進(jìn)隊(duì)列。Reference本身也被實(shí)現(xiàn)成一個(gè)鏈表,當(dāng)一個(gè)Reference作為一個(gè)引用時(shí),其next為null,如果作為一個(gè)pending引用鏈出現(xiàn),next要么是this(鏈表尾),要么是其它的引用實(shí)例。

引用(Reference)的狀態(tài)

Reference將引用的狀態(tài)分為有效(Active)、掛起(Pending)、待處理(Enqueued)、不可用(Inactive)。通過判斷一個(gè)Reference實(shí)例是否被注冊(cè)(is registered)到該Reference實(shí)例來影響一個(gè)引用被GC后的狀態(tài)變化。

  • 有效(Active):新創(chuàng)建的引用實(shí)例,在其被引用的對(duì)象被回收之前是有效的
  • 掛起(Pending):其被引用的對(duì)象被回收之后被放到pending-Reference列表中,等待Reference-handler線程處理的引用
  • 待處理(Enqueued):一個(gè)在ReferenceQueue隊(duì)列實(shí)例中的引用
  • 無效(Inactive):不再可用的引用。

只有在一個(gè)被注冊(cè)(包含一個(gè)ReferenceQueue實(shí)例引用)的引用中可能存在掛起(Pending)和待處理(Enqueued)狀態(tài)。

引用(Reference)的生命周期

一個(gè)引用的生命周期通常是這樣子的:

首先,當(dāng)一個(gè)引用被創(chuàng)建時(shí),無論有沒有被注冊(cè),總是有效的(Active)。在GC標(biāo)記階段,如果一個(gè)referent被標(biāo)記為不可達(dá)(沒有GC root),收集器在檢測(cè)到referent的可達(dá)性發(fā)生變化(由可達(dá)變?yōu)椴豢蛇_(dá))后,如果一個(gè)引用是被注冊(cè)的,那么JVM會(huì)將該引用更改為掛起(Pending)狀態(tài),否則直接不可用(Inactive)。

怎么判斷一個(gè)引用是否被注冊(cè)呢?通過這個(gè)引用是否有持有一個(gè)非Null的ReferenceQueue實(shí)例。如果用戶沒有在構(gòu)造引用實(shí)例時(shí)手動(dòng)傳入一個(gè)ReferenceQueue,那么這個(gè)引用就是未被注冊(cè)的。這個(gè)Null也是一個(gè)類,是一個(gè)ReferenceQueue內(nèi)部狀態(tài)類,沒有別的作用,僅僅作為一個(gè)生成空對(duì)象的實(shí)例使用。

private static class Null extends ReferenceQueue {
    boolean enqueue(Reference r) {
        return false;
    }
}

若一個(gè)引用被注冊(cè),那么JVM會(huì)將該引用實(shí)例添加到pending-Reference列表中,并修改其next,該引用正式處于掛起(Pending)狀態(tài)。所謂的pending-Reference列表,就是Reference中的一個(gè)特殊的私有靜態(tài)引用,“添加到pending-Reference列表中”其實(shí)就是一個(gè)賦值(set)操作。當(dāng)一個(gè)引用被掛起(Pending)后,唯一的目的就是等待Reference-handler線程將其從pending-Reference列表中移動(dòng)到ReferenceQueue。

Reference-handler線程將掛起的引用從pending-Reference列表中移動(dòng)到它被注冊(cè)的ReferenceQueue后,這個(gè)引用的狀態(tài)就成了待處理(Enqueued)。由于ReferenceQueue是用戶指定的,所以用戶可以對(duì)這個(gè)狀態(tài)的引用進(jìn)行操作,也可以說,被注冊(cè)的引用在被GC后,用戶可以得到一個(gè)通知。ReferenceQueue也可以說是一個(gè)消息隊(duì)列,用戶可以對(duì)里面的引用進(jìn)行操作,典型的應(yīng)用就是WeakHashMap對(duì)弱引用的處理。一旦里面的引用被移出隊(duì)列,那么該引用的狀態(tài)就會(huì)變?yōu)樽罱K態(tài)——無效(Inactive)狀態(tài)。無效狀態(tài)的引用再也不會(huì)更改為其他狀態(tài),只能等待自身被GC。

再談pending-Reference列表

pending-Reference列表,就是Reference中的一個(gè)特殊的私有靜態(tài)引用:

private static Reference pending = null;

與之類似的還有discovered

transient private Reference<T> discovered;

為什么說這兩個(gè)變量特殊,是因?yàn)镴ava中沒有任何對(duì)該引用賦值的定義,那么如何將引用實(shí)例放入pending字段中呢?這由VM對(duì)字節(jié)碼的調(diào)用完成。openjdk中的hotspot源碼中,hotspot/src/share/vm/memory/referenceProcessor.cpp這個(gè)文件中有一個(gè)ReferenceProcessor::discover_reference方法,根據(jù)此方法的注釋由了解到虛擬機(jī)在對(duì)Reference的處理有ReferenceBasedDiscovery和RefeferentBasedDiscovery兩種策略。這兩個(gè)策略的實(shí)現(xiàn)不在討論范圍內(nèi),此處省略不提??傊?,VM通過對(duì)Reference的操作,實(shí)現(xiàn)了引用狀態(tài)的變更,由于這些類都是在java.lang下,所以這也是用戶手動(dòng)繼承實(shí)現(xiàn)一個(gè)引用類可能會(huì)無效的原因了。

Reference-handler線程

在Reference內(nèi)部有一個(gè)類叫ReferenceHandler,它繼承了Thread,是Reference-handler的實(shí)現(xiàn)。這個(gè)類主要的作用就是將pending中的鏈表節(jié)點(diǎn)逐個(gè)移動(dòng)到Reference實(shí)例的ReferenceQueue中,最終將pending還原為null,如果pending為null,這個(gè)線程將會(huì)無限期掛起。

這個(gè)類是在Reference的靜態(tài)代碼塊中實(shí)例化并運(yùn)行的,由類加載的知識(shí)可以知道類的初始化在第一次使用這個(gè)類的時(shí)候在其之前完成,所以當(dāng)用戶決定使用一個(gè)Reference子類時(shí),就會(huì)開始這個(gè)線程。線程默認(rèn)的優(yōu)先級(jí)就是最高的優(yōu)先級(jí)MAX_PRIORITY,如果某個(gè)系統(tǒng)擁有比MAX_PRIORITY還要高得等級(jí),該線程也會(huì)和內(nèi)核線程同等優(yōu)先級(jí)運(yùn)行??傊?,開始這個(gè)線程之前,Reference會(huì)保證這個(gè)線程以最高優(yōu)先級(jí)運(yùn)行。同時(shí),這也是一個(gè)守護(hù)線程。

如果用戶線程已經(jīng)全部退出運(yùn)行了,只剩下守護(hù)線程存在了,那么虛擬機(jī)也會(huì)退出,即退出程序。 因?yàn)闆]有了被守護(hù)者,守護(hù)線程也就沒有工作可做了,也就沒有繼續(xù)運(yùn)行程序的必要了。

  • thread.setDaemon(true)必須在thread.start()之前設(shè)置,否則會(huì)跑出一個(gè)IllegalThreadStateException異常。你不能把正在運(yùn)行的常規(guī)線程設(shè)置為守護(hù)線程。
  • 在Daemon線程中產(chǎn)生的新線程也是Daemon的。
  • 守護(hù)線程應(yīng)該永遠(yuǎn)不去訪問固有資源,如文件、數(shù)據(jù)庫,因?yàn)樗鼤?huì)在任何時(shí)候甚至在一個(gè)操作的中間發(fā)生中斷。

enqueued 操作

Reference-handler線程的enqueued操作是通過調(diào)用Reference的ReferenceQueue實(shí)現(xiàn)的。本質(zhì)就是調(diào)用ReferenceQueue的enqueued方法,傳入需要enqueued的引用。enqueued方法將該引用的狀態(tài)更改為ENQUEUED,此時(shí)這個(gè)引用的ReferenceQueue被替換成Null類,然后使用頭插法把這個(gè)引用插入這個(gè)隊(duì)列的隊(duì)頭里。如果已經(jīng)是ENQUEUED狀態(tài)的引用會(huì)直接退出方法。

這個(gè)方法,如果enqueued操作成功,即成功將一個(gè)引用插入隊(duì)列,則返回true,其他情況返回false。

enqueued操作會(huì)鎖定傳入的引用對(duì)象,所以是同步的,而且入隊(duì)時(shí)會(huì)進(jìn)一步鎖定隊(duì)列,防止并發(fā)情況下插入失敗。

引用鎖

上文提到,如果pending為null,Reference-handler線程將會(huì)無限期掛起。那么總是要喚醒這個(gè)線程的,在哪里喚醒這個(gè)線程呢?要聊到這個(gè)話題,就要聊到Reference的鎖。java.lang.ref.Reference<T>java.lang.ref.ReferenceQueue<T>中,各有一個(gè)自定義的鎖類,上文提到的對(duì)象狀態(tài)變更需要的同步操作,都需要持有這兩個(gè)鎖類的鎖才能完成。兩個(gè)類對(duì)鎖的定義都很簡(jiǎn)單,就是一個(gè)空的類。

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();

唯一的區(qū)別就是Reference的鎖類引用帶有static,帶有static是因?yàn)閷?duì)象用于與垃圾收集器同步。收集器必須在每個(gè)收集周期的開始處獲取此鎖。因此任何持有此鎖的代碼盡可能快地完成,不應(yīng)該在持有這個(gè)鎖的時(shí)候分配新對(duì)象,而且應(yīng)避免調(diào)用用戶代碼。

可是代碼中并沒有Reference中鎖的任何類似調(diào)用nolify方法等的喚醒操作,所以筆者認(rèn)為,喚醒操作應(yīng)該也是在JVM內(nèi)部實(shí)現(xiàn)的。至于時(shí)機(jī),可能是當(dāng)一次GC結(jié)束后。

而ReferenceQueue的鎖相對(duì)簡(jiǎn)單。當(dāng)某個(gè)線程執(zhí)行remove操作時(shí),如果是空隊(duì)列,則掛起這個(gè)線程,僅當(dāng)達(dá)到Timeout或者執(zhí)行enqueue操作才會(huì)被喚醒。由于只通過引用類調(diào)用,所以只有當(dāng)狀態(tài)更改時(shí)才會(huì)喚醒。Finalize線程會(huì)調(diào)用remove方法,這里不再詳述。

Finalizer和FinalReference

finalize的執(zhí)行也大同小異,都是通過static語句塊啟動(dòng)一個(gè)線程,只是這里啟動(dòng)的是低優(yōu)先級(jí)的線程。,而且最終的調(diào)用邏輯是通過sun.misc.JavaLangAccess類完成的。當(dāng)然Runtime.runFinalization()方法和java.lang.Shutdown類通過調(diào)用native方法,再通過native中回調(diào)Finalizer中的runAllFinalizers方法也能執(zhí)行finalize的調(diào)用。至于finalize是如何調(diào)用的,網(wǎng)上有博客,我就不再贅述了。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容