引用的分類
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實例用于實現(xiàn)通知蔓搞。
JDK中的引用(Reference)
Java使用java.lang.ref
下的類表示和管理對象的引用狀態(tài),如上面提到的三種其他引用随橘,以及finalization在Java語言層上的實現(xiàn)喂分。通過這些類與JVM進行交互,共同實現(xiàn)Java對這些引用的邏輯机蔗。除了強引用外蒲祈,Java通過java.lang.ref.Reference<T>
實現(xiàn)其他類型的引用。Reference中定義了引用的狀態(tài)(State)萝嘁,當(dāng)發(fā)生一次GC后梆掸,某些引用的狀態(tài)會隨之發(fā)生改變。狀態(tài)改變后牙言,某些引用可以通過放置到用戶指定的java.lang.ref.ReferenceQueue
實例酸钦,實現(xiàn)被引用的對象失效后對引用實例本身的操作,比如在引用失效后通知給用戶咱枉。
Java所有的除了強引用之外的引用都通過java.lang.ref.Reference<T>
抽象類實現(xiàn)钝鸽,該類某些邏輯是通過與JVM的操作緊密結(jié)合而實現(xiàn)的,所以除了java.lang.ref
下繼承它的子類可以被JVM識別庞钢,自己繼承這個抽象類是沒有任何意義的拔恰。Reference通過一個referent
的泛型引用保存被引用的對象,同時也持有一個queue引用保存一個ReferenceQueue<? super T>
的實例用于對引用的注冊(register)操作基括,用于在被引用對象失效后將引用注冊進隊列颜懊。Reference本身也被實現(xiàn)成一個鏈表,當(dāng)一個Reference作為一個引用時风皿,其next為null河爹,如果作為一個pending引用鏈出現(xiàn),next要么是this(鏈表尾)桐款,要么是其它的引用實例咸这。
引用(Reference)的狀態(tài)
Reference將引用的狀態(tài)分為有效(Active)、掛起(Pending)魔眨、待處理(Enqueued)媳维、不可用(Inactive)酿雪。通過判斷一個Reference實例是否被注冊(is registered)到該Reference實例來影響一個引用被GC后的狀態(tài)變化。
- 有效(Active):新創(chuàng)建的引用實例侄刽,在其被引用的對象被回收之前是有效的
- 掛起(Pending):其被引用的對象被回收之后被放到pending-Reference列表中指黎,等待Reference-handler線程處理的引用
- 待處理(Enqueued):一個在ReferenceQueue隊列實例中的引用
- 無效(Inactive):不再可用的引用。
只有在一個被注冊(包含一個ReferenceQueue實例引用)的引用中可能存在掛起(Pending)和待處理(Enqueued)狀態(tài)州丹。
引用(Reference)的生命周期
一個引用的生命周期通常是這樣子的:
首先醋安,當(dāng)一個引用被創(chuàng)建時,無論有沒有被注冊墓毒,總是有效的(Active)吓揪。在GC標(biāo)記階段,如果一個referent被標(biāo)記為不可達(沒有GC root)所计,收集器在檢測到referent的可達性發(fā)生變化(由可達變?yōu)椴豢蛇_)后柠辞,如果一個引用是被注冊的,那么JVM會將該引用更改為掛起(Pending)狀態(tài)醉箕,否則直接不可用(Inactive)。
怎么判斷一個引用是否被注冊呢徙垫?通過這個引用是否有持有一個非Null的ReferenceQueue實例讥裤。如果用戶沒有在構(gòu)造引用實例時手動傳入一個ReferenceQueue腾窝,那么這個引用就是未被注冊的奴璃。這個Null也是一個類,是一個ReferenceQueue內(nèi)部狀態(tài)類夸溶,沒有別的作用吴旋,僅僅作為一個生成空對象的實例使用损肛。
private static class Null extends ReferenceQueue {
boolean enqueue(Reference r) {
return false;
}
}
若一個引用被注冊,那么JVM會將該引用實例添加到pending-Reference列表中荣瑟,并修改其next治拿,該引用正式處于掛起(Pending)狀態(tài)。所謂的pending-Reference列表笆焰,就是Reference中的一個特殊的私有靜態(tài)引用劫谅,“添加到pending-Reference列表中”其實就是一個賦值(set)操作。當(dāng)一個引用被掛起(Pending)后嚷掠,唯一的目的就是等待Reference-handler線程將其從pending-Reference列表中移動到ReferenceQueue捏检。
Reference-handler線程將掛起的引用從pending-Reference列表中移動到它被注冊的ReferenceQueue后,這個引用的狀態(tài)就成了待處理(Enqueued)不皆。由于ReferenceQueue是用戶指定的贯城,所以用戶可以對這個狀態(tài)的引用進行操作,也可以說霹娄,被注冊的引用在被GC后能犯,用戶可以得到一個通知鲫骗。ReferenceQueue也可以說是一個消息隊列,用戶可以對里面的引用進行操作悲雳,典型的應(yīng)用就是WeakHashMap對弱引用的處理挎峦。一旦里面的引用被移出隊列,那么該引用的狀態(tài)就會變?yōu)樽罱K態(tài)——無效(Inactive)狀態(tài)合瓢。無效狀態(tài)的引用再也不會更改為其他狀態(tài)坦胶,只能等待自身被GC。
再談pending-Reference列表
pending-Reference列表晴楔,就是Reference中的一個特殊的私有靜態(tài)引用:
private static Reference pending = null;
與之類似的還有discovered
transient private Reference<T> discovered;
為什么說這兩個變量特殊顿苇,是因為Java中沒有任何對該引用賦值的定義,那么如何將引用實例放入pending字段中呢税弃?這由VM對字節(jié)碼的調(diào)用完成纪岁。openjdk中的hotspot源碼中,hotspot/src/share/vm/memory/referenceProcessor.cpp
這個文件中有一個ReferenceProcessor::discover_reference
方法则果,根據(jù)此方法的注釋由了解到虛擬機在對Reference的處理有ReferenceBasedDiscovery和RefeferentBasedDiscovery兩種策略幔翰。這兩個策略的實現(xiàn)不在討論范圍內(nèi),此處省略不提西壮∫旁觯總之,VM通過對Reference的操作款青,實現(xiàn)了引用狀態(tài)的變更做修,由于這些類都是在java.lang
下,所以這也是用戶手動繼承實現(xiàn)一個引用類可能會無效的原因了抡草。
Reference-handler線程
在Reference內(nèi)部有一個類叫ReferenceHandler
饰及,它繼承了Thread,是Reference-handler的實現(xiàn)康震。這個類主要的作用就是將pending中的鏈表節(jié)點逐個移動到Reference實例的ReferenceQueue中燎含,最終將pending還原為null,如果pending為null腿短,這個線程將會無限期掛起瘫镇。
這個類是在Reference的靜態(tài)代碼塊中實例化并運行的,由類加載的知識可以知道類的初始化在第一次使用這個類的時候在其之前完成答姥,所以當(dāng)用戶決定使用一個Reference子類時铣除,就會開始這個線程。線程默認(rèn)的優(yōu)先級就是最高的優(yōu)先級MAX_PRIORITY
鹦付,如果某個系統(tǒng)擁有比MAX_PRIORITY
還要高得等級尚粘,該線程也會和內(nèi)核線程同等優(yōu)先級運行∏贸ぃ總之郎嫁,開始這個線程之前秉继,Reference會保證這個線程以最高優(yōu)先級運行。同時泽铛,這也是一個守護線程尚辑。
如果用戶線程已經(jīng)全部退出運行了,只剩下守護線程存在了盔腔,那么虛擬機也會退出杠茬,即退出程序。 因為沒有了被守護者弛随,守護線程也就沒有工作可做了瓢喉,也就沒有繼續(xù)運行程序的必要了。
- thread.setDaemon(true)必須在thread.start()之前設(shè)置舀透,否則會跑出一個IllegalThreadStateException異常栓票。你不能把正在運行的常規(guī)線程設(shè)置為守護線程。
- 在Daemon線程中產(chǎn)生的新線程也是Daemon的愕够。
- 守護線程應(yīng)該永遠不去訪問固有資源走贪,如文件、數(shù)據(jù)庫惑芭,因為它會在任何時候甚至在一個操作的中間發(fā)生中斷坠狡。
enqueued 操作
Reference-handler線程的enqueued操作是通過調(diào)用Reference的ReferenceQueue實現(xiàn)的。本質(zhì)就是調(diào)用ReferenceQueue的enqueued方法强衡,傳入需要enqueued的引用擦秽。enqueued方法將該引用的狀態(tài)更改為ENQUEUED
码荔,此時這個引用的ReferenceQueue被替換成Null類漩勤,然后使用頭插法把這個引用插入這個隊列的隊頭里。如果已經(jīng)是ENQUEUED
狀態(tài)的引用會直接退出方法缩搅。
這個方法越败,如果enqueued操作成功,即成功將一個引用插入隊列硼瓣,則返回true究飞,其他情況返回false。
enqueued操作會鎖定傳入的引用對象堂鲤,所以是同步的亿傅,而且入隊時會進一步鎖定隊列,防止并發(fā)情況下插入失敗瘟栖。
引用鎖
上文提到葵擎,如果pending為null,Reference-handler線程將會無限期掛起半哟。那么總是要喚醒這個線程的酬滤,在哪里喚醒這個線程呢签餐?要聊到這個話題,就要聊到Reference的鎖盯串。java.lang.ref.Reference<T>
和java.lang.ref.ReferenceQueue<T>
中氯檐,各有一個自定義的鎖類,上文提到的對象狀態(tài)變更需要的同步操作体捏,都需要持有這兩個鎖類的鎖才能完成冠摄。兩個類對鎖的定義都很簡單,就是一個空的類译打。
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īng)該在持有這個鎖的時候分配新對象,而且應(yīng)避免調(diào)用用戶代碼韵洋。
可是代碼中并沒有Reference中鎖的任何類似調(diào)用nolify方法等的喚醒操作竿刁,所以筆者認(rèn)為,喚醒操作應(yīng)該也是在JVM內(nèi)部實現(xiàn)的搪缨。至于時機食拜,可能是當(dāng)一次GC結(jié)束后。
而ReferenceQueue的鎖相對簡單副编。當(dāng)某個線程執(zhí)行remove操作時负甸,如果是空隊列,則掛起這個線程痹届,僅當(dāng)達到Timeout或者執(zhí)行enqueue操作才會被喚醒呻待。由于只通過引用類調(diào)用,所以只有當(dāng)狀態(tài)更改時才會喚醒队腐。Finalize線程會調(diào)用remove方法蚕捉,這里不再詳述。
Finalizer和FinalReference
finalize的執(zhí)行也大同小異柴淘,都是通過static語句塊啟動一個線程迫淹,只是這里啟動的是低優(yōu)先級的線程。为严,而且最終的調(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)上有博客,我就不再贅述了。