Java實現(xiàn)的引用

引用的分類

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)上有博客,我就不再贅述了。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末瑞妇,一起剝皮案震驚了整個濱河市稿静,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌辕狰,老刑警劉巖改备,帶你破解...
    沈念sama閱讀 217,185評論 6 503
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異蔓倍,居然都是意外死亡悬钳,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,652評論 3 393
  • 文/潘曉璐 我一進店門偶翅,熙熙樓的掌柜王于貴愁眉苦臉地迎上來默勾,“玉大人,你說我怎么就攤上這事聚谁∧赴” “怎么了?”我有些...
    開封第一講書人閱讀 163,524評論 0 353
  • 文/不壞的土叔 我叫張陵形导,是天一觀的道長环疼。 經(jīng)常有香客問我,道長朵耕,這世上最難降的妖魔是什么炫隶? 我笑而不...
    開封第一講書人閱讀 58,339評論 1 293
  • 正文 為了忘掉前任,我火速辦了婚禮阎曹,結(jié)果婚禮上伪阶,老公的妹妹穿的比我還像新娘。我一直安慰自己处嫌,他們只是感情好栅贴,可當(dāng)我...
    茶點故事閱讀 67,387評論 6 391
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著锰霜,像睡著了一般筹误。 火紅的嫁衣襯著肌膚如雪桐早。 梳的紋絲不亂的頭發(fā)上癣缅,一...
    開封第一講書人閱讀 51,287評論 1 301
  • 那天,我揣著相機與錄音哄酝,去河邊找鬼友存。 笑死,一個胖子當(dāng)著我的面吹牛陶衅,可吹牛的內(nèi)容都是我干的屡立。 我是一名探鬼主播,決...
    沈念sama閱讀 40,130評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼搀军,長吁一口氣:“原來是場噩夢啊……” “哼膨俐!你這毒婦竟也來了勇皇?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 38,985評論 0 275
  • 序言:老撾萬榮一對情侶失蹤焚刺,失蹤者是張志新(化名)和其女友劉穎敛摘,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體乳愉,經(jīng)...
    沈念sama閱讀 45,420評論 1 313
  • 正文 獨居荒郊野嶺守林人離奇死亡兄淫,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,617評論 3 334
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了蔓姚。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片捕虽。...
    茶點故事閱讀 39,779評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖坡脐,靈堂內(nèi)的尸體忽然破棺而出泄私,到底是詐尸還是另有隱情,我是刑警寧澤备闲,帶...
    沈念sama閱讀 35,477評論 5 345
  • 正文 年R本政府宣布挖滤,位于F島的核電站,受9級特大地震影響浅役,放射性物質(zhì)發(fā)生泄漏斩松。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 41,088評論 3 328
  • 文/蒙蒙 一觉既、第九天 我趴在偏房一處隱蔽的房頂上張望惧盹。 院中可真熱鬧,春花似錦瞪讼、人聲如沸钧椰。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,716評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽嫡霞。三九已至,卻和暖如春希柿,著一層夾襖步出監(jiān)牢的瞬間诊沪,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 32,857評論 1 269
  • 我被黑心中介騙來泰國打工曾撤, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留端姚,地道東北人。 一個月前我還...
    沈念sama閱讀 47,876評論 2 370
  • 正文 我出身青樓挤悉,卻偏偏與公主長得像渐裸,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 44,700評論 2 354

推薦閱讀更多精彩內(nèi)容