JAVA對象引用體系除了強引用以外,出於對性能、可擴展性等方面考慮還特意實現了四種其餘引用:SoftReference、WeakReference、PhantomReference、FinalReference,本文主要想講的是FinalReference,由於咱們在使用內存分析工具好比mat等在分析一些oom的heap的時候,常常能看到 java.lang.ref.Finalizer佔用的內存大小遠遠排在前面(其實經過jmap -histo就能發現,以下圖所示),而這個類佔用的內存大小又和咱們此次的主角FinalReference有着密不可分的關係。java
對於FinalReference及關聯的內容,咱們可能有以下印象:算法
那FinalReference到底存在的意義是什麼,以怎樣的形式和咱們的代碼相關聯呢,這是本文要理清的問題。jvm
首先咱們看看FinalReference在JDK裏的實現:socket
你們應該注意到了類訪問權限是package的,這也就意味着咱們不能直接去對其進行擴展,可是JDK裏對此類進行了擴展實現java.lang.ref.Finalizer,這個類也是咱們在概述裏提到的,而此類的訪問權限也是package的,而且是final的,意味着真的不能被擴展了,接下來的重點咱們圍繞java.lang.ref.Finalizer展開(PS:後續講Finalizer相關的其實也就是在說FinalReference)函數
從構造函數上咱們得到下面的幾個關鍵信息工具
private:意味着咱們在外面沒法本身構建這類對象性能
finalizee參數:FinalReference指向的對象引用線程
調用add方法:將當前對象插入到Finalizer對象鏈裏,鏈裏的對象和Finalizer類靜態相關聯,言外之意是在這個鏈裏的對象都沒法被gc掉,除非將這種引用關係剝離掉(由於Finalizer類沒法被unload)。3d
雖然外面沒法建立Finalizer對象,可是注意到有一個register的靜態方法,在方法裏會建立這種對象,同時將這個對象加入到Finalizer對象鏈裏,這個方法是被vm調用的,那麼問題來了,vm在什麼狀況下會調用這個方法呢?cdn
類其實有挺多的修飾,好比final,abstract,public等等,若是一個類有final修飾,咱們就說這個類是一個final類,上面列的都是語法層面咱們能夠顯示標記的,在jvm裏其實還給類標記其餘一些符號,好比finalizer,表示這個類是一個finalizer類(爲了和java.lang.ref.Fianlizer類進行區分,下文要提到的finalizer類的地方都說成f類),gc在處理這種類的對象的時候要作一些特殊的處理,如在這個對象被回收以前會調用一下它的finalize方法。
在講這個問題以前,咱們先來看下java.lang.Object裏的一個方法
在Object類裏定義了一個名爲finalize的空方法,這意味着Java世界裏的全部類都會繼承這個方法,甚至能夠覆寫該方法,而且根據方法覆寫原則,若是子類覆蓋此方法,方法訪問權限都是至少是protected級別的,這樣其子類就算沒有覆寫此方法也會繼承此方法。
而判斷當前類是不是一個f類的標準並不只僅是當前類是否含有一個參數爲空,返回值爲void的名爲finalize的方法,而另一個要求是finalize方法必須非空,所以咱們的Object類雖然含有一個finalize方法,可是並非一個f類,Object的對象在被gc回收的時候其實並不會去調用它的finalize方法。
須要注意的是咱們的類在被加載過程當中其實就已經被標記爲是否爲f類了(遍歷全部方法,包括父類的方法,只要有一個非空的參數爲空返回void的finalize方法就認爲是一個f類)。
對象的建立實際上是被拆分紅多個步驟的,好比A a=new A(2)這樣一條語句對應的字節碼以下:
先執行new分配好對象空間,而後再執行invokespecial調用構造函數,jvm裏其實可讓用戶選擇在這兩個時機中的任意一個將當前對象傳遞給Finalizer.register方法來註冊到Finalizer對象鏈裏,這個選擇依賴於RegisterFinalizersAtInit這個vm參數是否被設置,默認值爲true,也就是在調用構造函數返回以前調用Finalizer.register方法,若是經過-XX:-RegisterFinalizersAtInit關閉了該參數,那將在對象空間分配好以後就將這個對象註冊進去。
另外須要提一點的是當咱們經過clone的方式複製一個對象的時候,若是當前類是一個f類,那麼在clone完成的時候將調用Finalizer.register方法進行註冊。
這個實現比較有意思,在這裏簡單提一下,咱們知道一個構造函數執行的時候,會去調用父類的構造函數,主要是爲了能對繼承自父類的屬性也能作初始化,那麼任何一個對象的初始化最終都會調用到Object的空構造函數裏(任何空的構造函數其實並不空,會含有三條字節碼指令,以下代碼所示),爲了避免對全部的類的構造函數都作埋點調用Finalizer.register方法,hotspot的實現是在Object這個類在作初始化的時候將構造函數裏的return指令替換爲returnregister_finalizer指令,該指令並非標準的字節碼指令,是hotspot擴展的指令,這樣在處理該指令的時候調用Finalizer.register方法,這樣就在侵入性很小的狀況下完美地解決了這個問題。
在Finalizer類的clinit方法(靜態塊)裏咱們看到它會建立了一個FinalizerThread的守護線程,這個線程的優先級並非最高的,意味着在cpu很緊張的狀況下其被調度的優先級可能會受到影響
這個線程主要就是從queue裏取Finalizer對象,而後執行該對象的runFinalizer方法,這個方法主要是將Finalizer對象從Finalizer對象鏈裏剝離出來,這樣意味着下次gc發生的時候就可能將其關聯的f對象gc掉了,最後將這個Finalizer對象關聯的f對象傳給了一個native方法invokeFinalizeMethod
其實invokeFinalizeMethod方法就是調了這個f對象的finalize方法,看到這裏你們應該恍然大悟了,整個過程都串起來了
那究竟何時會將Finalizer對象丟到ReferenceQueue裏呢?當gc發生的時候,gc算法會判斷f類對象是否是隻被Finalizer類引用(f類對象被Finalizer對象引用,而後放到Finalizer對象鏈裏),若是這個對象僅僅被Finalizer對象引用的時候,說明這個對象在不久的未來會被回收了,能夠執行它的finalize方法了,因而會將這個Finalizer對象放到Reference.pending字段裏(是一個Reference對象,可是是鏈式的)。可是這個f類對象其實並無被回收,由於Finalizer這個類還對他們持有引用,在gc完成以前,jvm會調用java.lang.ref.Reference裏的lock對象的notify方法(當Reference.pending爲空的時候,有個專門處理引用的叫作ReferenceHandler的線程會一直在wait),ReferenceHandler這個線程在處理的時候會將對應的Finalizer對象丟到Finalizer類的ReferenceQueue裏,此時由於ReferenceQueue非空了,因而FinalizerThread會執行上面FinalizeThread線程裏看到的其餘邏輯了。這個過程可能有點繞,最好是結合代碼看看,下面簡單繪了一個圖
不知道你們有沒有想過若是f對象的finalize方法拋了一個沒捕獲的異常,這個FinalizerThread會不會退出呢,細心的讀者看上面的代碼其實就能夠找到答案,在runFinalizer方法裏對Throwable的異常都進行了捕獲,所以不可能出現FinalizerThread因異常未捕獲而退出的狀況。
若是咱們在f對象的finalize方法裏從新將當前對象賦值出去,變成可達對象,當這個f對象再次變成不可達的時候還會被執行finalize方法嗎?答案是否認的,由於在執行完第一次finalize方法以後,這個f對象已經和以前的Finalizer對象關係剝離了,也就是下次gc的時候不會再發現Finalizer對象指向該f對象了,天然也就不會調用這個f對象的finalize方法了。
這裏舉一個簡單的例子,咱們使用挺廣的socket通訊,SocksSocketImpl的父類其實就實現了finalize方法:
其實這麼作的主要目的是萬一用戶忘記關閉socket了,那麼在這個對象被回收的時候能主動關閉socket來釋放一些系統資源,可是若是真的是用戶忘記關閉了,那這些socket對象可能由於FinalizeThread遲遲沒有執行到這些socket對象的finalize方法,而致使內存泄露,這種問題咱們碰到過屢次,須要特別注意的是對於已經沒有地方引用的這些f對象,並不會在最近的那一次gc裏立刻回收掉,而是會延遲到下一個或者下幾個gc時才被回收,由於執行finalize方法的動做沒法在gc過程當中執行,萬一finalize方法執行很長呢,因此只能在這個gc週期裏將這個垃圾對象從新標活,直到執行完finalize方法從queue裏刪除,這樣下次gc的時候就真的是漂浮垃圾了會被回收,所以給你們的一個建議是千萬不要在運行期不斷建立f對象,否則會很悲劇。
上面的過程基本對Finalizer的實現細節進行完整剖析了,java裏咱們看到有構造函數,可是並無看到析構函數一說,Finalizer實際上是實現了析構函數的概念,咱們在對象被回收前能夠執行一些『收拾性』的邏輯,應該說是一個特殊場景的補充,可是這種概念的實現給咱們的f對象生命週期以及gc等帶來了一些影響:
推薦閱讀 ZGC何時會進行垃圾回收
推薦閱讀 GC一些長時間停頓問題排查及解決辦法