新聞中心
概述

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


咨詢
建站咨詢
