從一個 Young GC 變慢的案例來聊聊 finalize 方法
背景
有一次一位同學上線之後,發現Young GC的時間飆升很多,監控如下圖:
監控顯示老程式碼(04機器)的平均young gc時間之後23ms,而新程式碼(01機器)為平均時間84ms。
上線去檢視gc log,新程式碼的gc log如下:
老程式碼的gc log 如下:
從上圖截圖可以發現:新上線的程式碼Object Copy階段時間上升了20ms左右,Ref Proc時間上升了45ms。導致整個young gc時間上升了60ms+。
把新上線的機器上的堆記憶體dump下來,使用MTA開啟之後,發現有很多java.lang.ref.Finalizer物件,這個物件引用了業務物件。檢視這個業務物件發現他實現了Object中的finalize方法,刪除 finalize方法上線之後,young gc恢復正常。
那麼為什麼在一個物件中加入 finalize方法之後,young gc時間會長這麼多,並且是消耗在 Copy階段和 Ref Proc階段。
finalize方法如何影響 GC執行的
在Object中有對 finalize方法如何工作的做出了說明,可以轉述為:“ 在子類實現了 finalize方法時,當 垃圾回收器確定該物件沒有任何引用時,就會呼叫 finalize方法,並且finalize方法最多被呼叫一次”。
JVM是如何實現finalize方法的呢?
-
JVM在載入類的時候,會去識別該類是否實現了 finalize方法並且該方法體不會空;若是含有有意義的 finalize方法體會標記出該類為“ finalize Class”。
-
在new “finalize Class”物件時,會呼叫 Finalizer.register方法,在該方法中new 一個Finalizer物件, Finalizer物件 會引用原始物件,然後把 Finalizer物件註冊到 Finalizer物件鏈裡(這樣就可以保證 Finalizer物件一直可達的 )。具體程式碼如下:
當然這步可以使用RegisterFinalizersAtInit這個JVM引數改變註冊到 Finalizer物件鏈中的時機。因為new 一個物件至少分為兩步:1.分配記憶體空間、2.呼叫建構函式。RegisterFinalizersAtInit預設是true,也就是這兩步都完成之後再註冊到 Finalizer物件鏈;如果改成false,會在分配記憶體完成之後呼叫建構函式之前註冊到 Finalizer物件鏈中。
-
在發生gc的時候,在判斷原始物件除了 Finalizer物件引用之外,沒有其他物件引用之後,就把 Finalizer物件從物件鏈中取出,加入到 Finalizer queue佇列中。
-
JVM在啟動時,會建立一個“ finalize ”執行緒,該執行緒會一直從“ Finalizer queue ”佇列中取出物件,然後執行原始物件中的 finalize方法。
-
在完成步驟4中, Finalizer物件以及其引用的原始物件,再也沒有其他物件引用他們,屬於不可達物件,再次GC的時候他們將會被回收掉。(如果在 finalize方法重新使該物件再次可達,再次GC該物件也不會被回收 )。
使用finalize方法帶來哪些影響?
-
建立一個包含finalize方法的物件時,需要額外建立 Finalizer物件並且註冊到 Finalizer 物件鏈中;這樣就需要額外的記憶體空間,並且建立 finalize方法的物件的時間要長。 筆者在本機上測試, 建立 普通物件和 含finalize方法的物件 時間相差4倍左右 (迴圈10000建立一個不含任何變數的物件)。
-
和相比普通物件,含有 finalize方法的物件的生存週期變長,普通物件一次GC就可以回收;而 含有finalize方法的物件至少需要兩次gc,這樣就會導致young gc階段Object Copy階段時間上升 。
-
在gc時需要對 包含finalize方法的物件做特殊處理,比如識別該物件是否只有 Finalizer物件引用,把 Finalizer物件新增到 queue佇列這些都是在gc階段完成,需要額外處理時間,在young gc屬於 Ref Proc時間,必然導致 Ref Proc階段時間上升。
-
因為 “ finalize ”執行緒優先順序比較低, 如果cpu比較繁忙,可能會導致 queue佇列有擠壓,在經歷多次young gc之後 原始物件和 Finalizer物件就會進入 old區域,那麼這些物件只能等待old gc才能被釋放掉。
總結
使用finalize()方法本身會加重系統負擔、嚴重影響GC並且不能保證 finalize的呼叫時機等一系列問題。所以 對於 普通的程式開發人員還是忘記有該方法的存在 吧。他的應用場景也僅僅針對於防止資源洩漏等場景,但是如果僅僅內部呼叫也不需要實現 finalize()方法 。