總結
1.當JVM通過GC Roots可達性分析,判斷某對象可以被回收後,會判斷是否重寫了finalize方法,如果沒有,直接回收
2.如果重寫了,把該對象放入F-Queue隊列,有線程(一個級别很低的daemon線程)專門周遊并執行這些的finalize方法
3.執行finalize()後,等下一次GC時再判斷該類是否可被回收,如果是就回收
override finalize()函數的風險:
含有override finalize()的對象至少要經曆兩次GC才能被回收。拉長了對象生命周期,拖慢GC速度,增加了OOM風險
finalizer線程運作級别很低,有可能出現finalize速度跟不上對象建立速度,最終可能還是會OOM
詳細
我們現在來看一下自定義了(override)finalize()的對象(或是某個父類override finalize())是怎樣被GC回收的,首先需要注意的是,含有override finalize()的對象A建立要經曆以下3個步驟:
建立對象A執行個體
建立java.lang.ref.Finalizer對象執行個體F1,F1指向A和一個reference queue
(引用關系,F1—>A,F1—>ReferenceQueue,ReferenceQueue的作用先賣個關子)
使java.lang.ref.Finalizer的類對象引用F1
(這樣可以保持F1永遠不會被回收,除非解除Finalizer的類對象對F1的引用)
經過上述三個步驟,我們建立了這樣的一個引用關系:
java.lang.ref.Finalizer–>F1–>A,F1–>ReferenceQueue。GC過程如下所示:
如上圖所示,在發生minor gc時,即便一個對象A不被任何其他對象引用,隻要它含有override finalize(),就會最終被java.lang.ref.Finalizer類的一個對象F1引用,等等,如果新生代的對象都含有override finalize(),那豈不是無法GC?沒錯,這就是finalize()的第一個風險所在,對于剛才說的情況,minor gc會把所有活躍對象以及被java.lang.ref.Finalizer類對象引用的(實際)垃圾對象拷貝到下一個survivor區域,如果拷貝溢出,就将溢出的資料晉升到老年代,極端情況下,老年代的容量會被迅速填滿,于是讓人頭痛的full gc就離我們不遠了。
那麼含有override finalize()的對象什麼時候被GC呢?例如對象A,當第一次minor gc中發現一個對象隻被java.lang.ref.Finalizer類對象引用時,GC線程會把指向對象A的Finalizer對象F1塞入F1所引用的ReferenceQueue中,java.lang.ref.Finalizer類對象中包含了一個運作級别很低的daemon線程finalizer來異步地調用這些對象的finalize()方法,調用完之後,java.lang.ref.Finalizer類對象會清除自己對F1的引用。這樣GC線程就可以在下一次minor gc時将對象A回收掉。
也就是說一次minor gc中實際至少包含兩個操作:
将活躍對象拷貝到survivor區域中
以Finalizer類對象為根,周遊所有Finalizer對象,将隻被Finalizer對象引用的對象(對應的Finalizer對象)塞入Finalizer的ReferenceQueue中
可見Finalizer對象的多少也會直接影響minor gc的快慢。
包含有自定義finalizer方法的對象回收過程總結下來,有以下三個風險:
如果随便一個finalize()抛出一個異常,finallize線程會終止,很快地會由于f queue的不斷增長導緻OOM
finalizer線程運作級别很低,有可能出現finalize速度跟不上對象建立速度,最終可能還是會OOM,實際應用中一般會有富裕的CPU時間,是以這種OOM情況可能不太常出現
含有override finalize()的對象至少要經曆兩次GC才能被回收,嚴重拖慢GC速度,運氣不好的話直接晉升到老年代,可能會造成頻繁的full gc,進而影響這個系統的性能和吞吐率。
以上的三點還沒有考慮minor gc時為了分辨哪些對象隻被java.lang.ref.Finalizer類對象引用的開銷,講完了finalize()原理,我們回頭看看最初的那句話:JVM能夠保證一個對象在回收以前一定會調用一次它的finalize()方法。
含有override finalize()的對象在會收前必然會進入F QUEUE,但是JVM本身無法保證一個對象什麼時候被回收,因為GC的觸發條件是需要GC,是以JVM方法不保證finalize()的調用點,如果對象一直不被回收,就一直不調用,而調用了finalize(),也不代表對象就被回收了,隻有到了下一次GC時該對象才能真正被回收。另外一個關鍵點是一次,在調用過一次對象A的finalize()之後,就解除了Finalizer類對象和對象F1之間的引用關系,如果在finalize()中又将對象本身重新賦給另外一個引用(對象拯救),那這個對象在真正被GC前是不會再次調用finalize()的。
總結一下finalize()的兩個個問題:
沒有析構函數那樣明确的語義,調用時間由JVM确定,一個對象的生命周期中隻會調用一次
拉長了對象生命周期,拖慢GC速度,增加了OOM風險
回到最初的問題,對于那些需要釋放資源的操作,我們應該怎麼辦?effective java告訴我們,最好的做法是提供close()方法,并且告知上層應用在不需要該對象時一掉要調用這類接口,可以簡單的了解這類接口充當了析構函數。當然,在某些特定場景下,finalize()還是非常有用的,例如實作一個native對象的夥伴對象,這種夥伴對象提供一個類似close()接口可能不太友善,或者語義上不夠友好,可以在finalize()中去做native對象的析構。不過還是那句話,fianlize()永遠不是必須的,千萬不要把它當做析構函數,對于一個對性能有相當要求的應用或服務,從一開始就杜絕使用finalize()是最好的選擇。