JVM源碼分析之FinalReference完全解讀
概述
JAVA對象引用體系除了強引用之外,出于對性能、可擴展性等方面考慮還特地實現(xiàn)了四種其他引用:SoftReference、WeakReference、PhantomReference、FinalReference,本文主要想講的是FinalReference,因為我們在使用內(nèi)存分析工具比如zprofiler、mat等在分析一些oom的heap的時候,經(jīng)常能看到 java.lang.ref.Finalizer占用的內(nèi)存大小遠遠排在前面,而這個類占用的內(nèi)存大小又和我們這次的主角FinalReference有著密不可分的關(guān)系。
對于FinalReference及關(guān)聯(lián)的內(nèi)容,我們可能有如下印象:
- 自己代碼里從沒有使用過
- 線程dump之后,我們能看到一個叫做Finalizer的java線程
- 偶爾能注意到j(luò)ava.lang.ref.Finalizer的存在
- 我們在類里可能會寫finalize方法
那FinalReference到底存在的意義是什么,以怎樣的形式和我們的代碼相關(guān)聯(lián)呢,這是本文要理清的問題。
JDK中的FinalReference
首先我們看看FinalReference在JDK里的實現(xiàn):
大家應(yīng)該注意到了類訪問權(quán)限是package的,這也就意味著我們不能直接去對其進行擴展,但是JDK里對此類進行了擴展實現(xiàn)java.lang.ref.Finalizer,這個類也是我們在概述里提到的,而此類的訪問權(quán)限也是package的,并且是final的,意味著真的不能被擴展了,接下來的重點我們圍繞java.lang.ref.Finalizer展開(PS:后續(xù)講Finalizer相關(guān)的其實也就是在說FinalReference)
Finalizer的構(gòu)造函數(shù)
從構(gòu)造函數(shù)上我們獲得下面的幾個關(guān)鍵信息 * private:意味著我們在外面無法自己構(gòu)建這類對象 * finalizee參數(shù):FinalReference指向的對象引用 * 調(diào)用add方法:將當前對象插入到Finalizer對象鏈里,鏈里的對象和Finalizer類靜態(tài)相關(guān)聯(lián),言外之意是在這個鏈里的對象都無法被gc掉,除非將這種引用關(guān)系剝離掉(因為Finalizer類無法被unload)。
雖然外面無法創(chuàng)建Finalizer對象,但是注意到有一個register的靜態(tài)方法,在方法里會創(chuàng)建這種對象,同時將這個對象加入到Finalizer對象鏈里,這個方法是被vm調(diào)用的,那么問題來了,vm在什么情況下會調(diào)用這個方法呢?
Finalizer對象何時被注冊到Finalizer對象鏈里
類其實有挺多的修飾,比如final,abstract,public等等,如果一個類有final修飾,我們就說這個類是一個final類,上面列的都是語法層面我們可以顯示標記的,在jvm里其實還給類標記其他一些符號,比如finalizer,表示這個類是一個finalizer類(為了和java.lang.ref.Fianlizer類進行區(qū)分,下文要提到的finalizer類的地方都說成f類),gc在處理這種類的對象的時候要做一些特殊的處理,如在這個對象被回收之前會調(diào)用一下它的finalize方法。
如何判斷一個類是不是一個f類
在講這個問題之前,我們先來看下java.lang.Object里的一個方法
在Object類里定義了一個名為finalize的空方法,這意味著Java世界里的所有類都會繼承這個方法,甚至可以覆寫該方法,并且根據(jù)方法覆寫原則,如果子類覆蓋此方法,方法訪問權(quán)限都是至少是protected級別的,這樣其子類就算沒有覆寫此方法也會繼承此方法。
而判斷當前類是否是一個f類的標準并不僅僅是當前類是否含有一個參數(shù)為空,返回值為void的名為finalize的方法,而另外一個要求是finalize方法必須非空,因此我們的Object類雖然含有一個finalize方法,但是并不是一個f類,Object的對象在被gc回收的時候其實并不會去調(diào)用它的finalize方法。
需要注意的是我們的類在被加載過程中其實就已經(jīng)被標記為是否為f類了(遍歷所有方法,包括父類的方法,只要有一個非空的參數(shù)為空返回void的finalize方法就認為是一個f類)。
f類的對象何時傳到Finalizer.register方法
對象的創(chuàng)建其實是被拆分成多個步驟的,比如A a=new A(2)這樣一條語句對應(yīng)的字節(jié)碼如下:
先執(zhí)行new分配好對象空間,然后再執(zhí)行invokespecial調(diào)用構(gòu)造函數(shù),jvm里其實可以讓用戶選擇在這兩個時機中的任意一個將當前對象傳遞給Finalizer.register方法來注冊到Finalizer對象鏈里,這個選擇依賴于RegisterFinalizersAtInit這個vm參數(shù)是否被設(shè)置,默認值為true,也就是在調(diào)用構(gòu)造函數(shù)返回之前調(diào)用Finalizer.register方法,如果通過-XX:-RegisterFinalizersAtInit關(guān)閉了該參數(shù),那將在對象空間分配好之后就將這個對象注冊進去。
另外需要提一點的是當我們通過clone的方式復制一個對象的時候,如果當前類是一個f類,那么在clone完成的時候?qū)⒄{(diào)用Finalizer.register方法進行注冊。
hotspot如何實現(xiàn)f類對象在構(gòu)造函數(shù)執(zhí)行完畢后調(diào)用Finalizer.register
這個實現(xiàn)比較有意思,在這里簡單提一下,我們知道一個構(gòu)造函數(shù)執(zhí)行的時候,會去調(diào)用父類的構(gòu)造函數(shù),主要是為了能對繼承自父類的屬性也能做初始化,那么任何一個對象的初始化最終都會調(diào)用到Object的空構(gòu)造函數(shù)里(任何空的構(gòu)造函數(shù)其實并不空,會含有三條字節(jié)碼指令,如下代碼所示),為了不對所有的類的構(gòu)造函數(shù)都做埋點調(diào)用Finalizer.register方法,hotspot的實現(xiàn)是在Object這個類在做初始化的時候?qū)?gòu)造函數(shù)里的return指令替換為_return_register_finalizer指令,該指令并不是標準的字節(jié)碼指令,是hotspot擴展的指令,這樣在處理該指令的時候調(diào)用Finalizer.register方法,這樣就在侵入性很小的情況下***地解決了這個問題。
f類對象的GC回收
FinalizerThread線程
在Finalizer類的clinit方法(靜態(tài)塊)里我們看到它會創(chuàng)建了一個FinalizerThread的守護線程,這個線程的優(yōu)先級并不是***的,意味著在cpu很緊張的情況下其被調(diào)度的優(yōu)先級可能會受到影響
這個線程主要就是從queue里取Finalizer對象,然后執(zhí)行該對象的runFinalizer方法,這個方法主要是將Finalizer對象從Finalizer對象鏈里剝離出來,這樣意味著下次gc發(fā)生的時候就可能將其關(guān)聯(lián)的f對象gc掉了,***將這個Finalizer對象關(guān)聯(lián)的f對象傳給了一個native方法invokeFinalizeMethod
其實invokeFinalizeMethod方法就是調(diào)了這個f對象的finalize方法,看到這里大家應(yīng)該恍然大悟了,整個過程都串起來了
f對象的finalize方法拋出異常會導致FinalizeThread退出嗎
不知道大家有沒有想過如果f對象的finalize方法拋了一個沒捕獲的異常,這個FinalizerThread會不會退出呢,細心的讀者看上面的代碼其實就可以找到答案,在runFinalizer方法里對Throwable的異常都進行了捕獲,因此不可能出現(xiàn)FinalizerThread因異常未捕獲而退出的情況。
f對象的finalize方法會執(zhí)行多次嗎
如果我們在f對象的finalize方法里重新將當前對象賦值出去,變成可達對象,當這個f對象再次變成不可達的時候還會被執(zhí)行finalize方法嗎?答案是否定的,因為在執(zhí)行完***次finalize方法之后,這個f對象已經(jīng)和之前的Finalizer對象關(guān)系剝離了,也就是下次gc的時候不會再發(fā)現(xiàn)Finalizer對象指向該f對象了,自然也就不會調(diào)用這個f對象的finalize方法了。
Finalizer對象何時被放到ReferenceQueue里
除了這里要說的環(huán)節(jié)之外,整個過程大家應(yīng)該都比較清楚了。
當gc發(fā)生的時候,gc算法會判斷f類對象是不是只被Finalizer類引用(f類對象被Finalizer對象引用,然后放到Finalizer對象鏈里),如果這個類僅僅被Finalizer對象引用的時候,說明這個對象在不久的將來會被回收了現(xiàn)在可以執(zhí)行它的finalize方法了,于是會將這個Finalizer對象放到Finalizer類的ReferenceQueue里,但是這個f類對象其實并沒有被回收,因為Finalizer這個類還對他們持有引用,在gc完成之前,jvm會調(diào)用ReferenceQueue里的lock對象的notify方法(當ReferenceQueue為空的時候,F(xiàn)inalizerThread線程會調(diào)用ReferenceQueue的lock對象的wait方法直到被jvm喚醒),此時就會執(zhí)行上面FinalizeThread線程里看到的其他邏輯了。
Finalizer導致的內(nèi)存泄露
這里舉一個簡單的例子,我們使用挺廣的socket通信,SocksSocketImpl的父類其實就實現(xiàn)了finalize方法:
其實這么做的主要目的是萬一用戶忘記關(guān)閉socket了,那么在這個對象被回收的時候能主動關(guān)閉socket來釋放一些系統(tǒng)資源,但是如果真的是用戶忘記關(guān)閉了,那這些socket對象可能因為FinalizeThread遲遲沒有執(zhí)行到這些socket對象的finalize方法,而導致內(nèi)存泄露,這種問題我們碰到過多次,需要特別注意的是對于已經(jīng)沒有地方引用的這些f對象,并不會在最近的那一次gc里馬上回收掉,而是會延遲到下一個或者下幾個gc時才被回收,因為執(zhí)行finalize方法的動作無法在gc過程中執(zhí)行,萬一finalize方法執(zhí)行很長呢,所以只能在這個gc周期里將這個垃圾對象重新標活,直到執(zhí)行完finalize方法從queue里刪除,這樣下次gc的時候就真的是漂浮垃圾了會被回收,因此給大家的一個建議是千萬不要在運行期不斷創(chuàng)建f對象,不然會很悲劇。
Finalizer的客觀評價
上面的過程基本對Finalizer的實現(xiàn)細節(jié)進行完整剖析了,java里我們看到有構(gòu)造函數(shù),但是并沒有看到析構(gòu)函數(shù)一說,F(xiàn)inalizer其實是實現(xiàn)了析構(gòu)函數(shù)的概念,我們在對象被回收前可以執(zhí)行一些『收拾性』的邏輯,應(yīng)該說是一個特殊場景的補充,但是這種概念的實現(xiàn)給我們的f對象生命周期以及gc等帶來了一些影響:
- f對象因為Finalizer的引用而變成了一個臨時的強引用,即使沒有其他的強引用了,還是無法立即被回收
- f對象至少經(jīng)歷兩次GC才能被回收,因為只有在FinalizerThread執(zhí)行完了f對象的finalize方法的情況下才有可能被下次gc回收,而有可能期間已經(jīng)經(jīng)歷過多次gc了,但是一直還沒執(zhí)行f對象的finalize方法
- cpu資源比較稀缺的情況下FinalizerThread線程有可能因為優(yōu)先級比較低而延遲執(zhí)行f對象的finalize方法
- 因為f對象的finalize方法遲遲沒有執(zhí)行,有可能會導致大部分f對象進入到old分代,此時容易引發(fā)old分代的gc,甚至fullgc,gc暫停時間明顯變長
- f對象的finalize方法被調(diào)用了,但是這個對象其實還并沒有被回收,雖然可能在不久的將來會被回收
【本文是51CTO專欄作者李嘉鵬的原創(chuàng)文章,轉(zhuǎn)載請通過微信公眾號(你假笨,id:lovestblog)聯(lián)系作者本人獲取授權(quán)】