前言
在某本書上面曾經看到過, Hotspot VM 的 gc 是準確式GC, 我的理解就是 這一次 gc 之後 應該會把所有的 "垃圾對象" 清理掉
假設對應的 $FinalizedClazz 重寫了 finalize 方法, 並且有一個 沒有任何引用的實例 o, 那麼 在下一次 gc 的時候應該回收掉對象 o, 但是 自從看了這部分的代碼, 以及 結合一些調試工具, 調試代碼, 似乎 發現實際情況 和我理解的是不一樣的
那麼 這種情況下 o 多久會被回收呢 ?
測試代碼如下 :
package com.hx.test10;
/**
* FinalReferentLifeCycle
*
* @author Jerry.X.He <[email protected]>
* @version 1.0
* @date 2019-10-18 09:48
*/
public class Test09FinalReferentLifeCycle {
/**
* lock
*/
static Object lock = new Object();
// Test09FinalReferentLifeCycle
// select s from com.hx.test02.Test09FinalReferentLifeCycle$FinalizedClazz s
public static void main(String[] args) throws Exception {
FinalizedClazz obj = new FinalizedClazz("randomString");
obj = null;
System.gc();
Thread.sleep(1000);
System.gc();
// `obj` is removed
System.out.println(" end ... ");
}
/**
* FinalizedClazz
*
* @author Jerry.X.He <[email protected]>
* @version 1.0
* @date 2019-10-18 15:58
*/
static class FinalizedClazz {
// for debug
private String ident;
public FinalizedClazz(String ident) {
this.ident = ident;
}
protected void finalize() {
System.out.println(" do finalize, ident : " + ident);
// wait on FinalizerThread ?
// synchronized (lock) {
// try {
// lock.wait();
// } catch (Exception e) {
// e.printStackTrace();
// }
// }
}
}
}
接下來 我們會針對兩種情況討論, 假設 Test09FinalReferentLifeCycle$FinalizedClazz.finalize 正常的執行一些 清理的工作, 又或者是 執行一些耗時較長的阻塞的工作
還有就是, 我們會先使用 HSDB 來驗證結論, 然後 再結合具體的代碼調試來說明原因
測試代碼均在jdk8下面編譯, 以下部分代碼, 截圖基於 jdk8
問題的細節
1. 基於HSDB的調試 - 正常運行FinalizedClazz.finalize
1.1 首先在 第一個 System.gc 和 第二個 System.gc 打上斷點, 然後 調試啓動
1.2 然後 jps 找到當前進程, 複製進程號
1.3 然後 啓動 HSDB, 連接到 該VM進程
1.4 然後使用 OQL 查詢給定的 Test09FinalReferentLifeCycle$FinalizedClazz 的所有實例, 如下圖所示
我們可以發現 給定的 vm 裏面僅僅只有一個 Test09FinalReferentLifeCycle$FinalizedClazz 的實例, 那麼即爲 我們 main 方法裏面 new 的這一個 Test09FinalReferentLifeCycle$FinalizedClazz 的實例 obj
1.5 放開斷點運行到第二個System.gc, 也是第一個 System.gc 之後, 再來查詢 Test09FinalReferentLifeCycle$FinalizedClazz 的所有實例, 如下圖所示
我們可以發現 第一次 System.gc 之後, 這個 Test09FinalReferentLifeCycle$FinalizedClazz 的實例 obj 依然可以找到, 也就是 還沒有被 gc 掉, 並且下面 輸出了 obj.finalize 的相關日誌, "do finalize, ident : randomString", 表示 該對象的 finalize 的階段已經完成
1.6 繼續往下看, 走到第二次 System.gc 之後, 再來查詢 Test09FinalReferentLifeCycle$FinalizedClazz 的所有實例, 如下圖所示
可以看到 第二次 System.gc 之後, 這個 Test09FinalReferentLifeCycle$FinalizedClazz 的實例 obj 已經找不到了, 也就是 被 gc 掉了
2. 基於HSDB的調試 - 掛起FinalizedClazz.finalize
我們直接跳到重點, 第二次 System.gc 之前的情況均是一致的
2.1 走到第二次 System.gc 之後, 再來查詢 Test09FinalReferentLifeCycle$FinalizedClazz 的所有實例, 如下圖所示
可以看到 第二次 System.gc 之後, 這個 Test09FinalReferentLifeCycle$FinalizedClazz 的實例 obj 依然能夠被找到, 也就是 還沒有被 gc 掉
2.2 那麼 爲什麼呢 ?, 首先我們看一下 引用 obj 的地方吧
我們來看看什麼對象 在引用我們的 Test09FinalReferentLifeCycle$FinalizedClazz 的實例 obj 吧, 點擊 compute liveness
這裏的引用來自於 Finalizer. unfinalized, Finalizer. unfinalized 會直接, 或者間接的引用 Test09FinalReferentLifeCycle$FinalizedClazz 的實例 obj 對應的 Finalizer
Finalizer 本身有 prev, next 來構成一個雙向鏈表, Finalizer. unfinalized 組合 prev, next 關聯的鏈表就是 註冊了 finalize, 需要finalize的對象的 Finalizer 列表
這裏或許會爲我們找到一些方向呢 ?
3. 梳理一下流程
在 Finalizer.register 裏面打一個條件斷點, 條件如下, 斷點上下文如下圖所示
try {
Field field = finalizee.getClass().getDeclaredField("ident");
return field != null;
} catch(Exception e) {
return false;
}
return true;
在 Test09FinalReferentLifeCycle$FinalizedClazz.finalize 裏面打一個斷點
3.1 當創建 Test09FinalReferentLifeCycle$FinalizedClazz 的實例 obj 的時候, vm 發現這個對象 重寫了 finalize 方法, 然後 調用了 Finalizer.register, 傳入 創建的對象的引用[還尚未初始化], 創建 obj 對應的 Finalizer[implements FinalReference], 然後添加到 Finalizer.unfinalized 列表
3.2 然後第一次 System.gc 發現 Test09FinalReferentLifeCycle$FinalizedClazz 的實例 obj, 只有 FinalReference.referent 引用 obj, 判斷 obj 可以回收, 然後 在 process_discovered_references 的階段 process_phase3 將 obj 複製到 存活區, 然後 將 obj 對應的 Finalizer 移動到 Reference.pending 的列表, 然後 ReferenceHandler 線程將 Reference.pending 列表的 Reference 放入各自應該放入的 ReferenceQueue[部分內容可以參考 : https://blog.csdn.net/u011039332/article/details/102635876]
3.3 對應於我們這裏, FinalizerThread 從 Finalizer.queue 裏面獲取 Finalizer, 來處理 finialize 業務
3.4 然後第二次 System.gc, 針對 FinalizedClazz.finalize 正常或者阻塞我們分開討論
----3.4.1 正常運行的情況, 沒有其他任何引用引用 o 了, FinalReference.referent 引用 obj 的那個 FinalReference 在 FinalizedClazz.finalize 執行之前被從 Finalizer.unfinalized 列表裏面移除了, 因此 第二次 System.gc 之後 obj 被回收了
----3.4.2 阻塞FinalizedClazz.finalize的情況, FinalReference.referent 引用 obj 的那個 FinalReference 在 FinalizedClazz.finalize 執行之前被從 Finalizer.unfinalized 列表裏面移除了, 但是 從上圖可知 至少棧幀中還有一個 finalizee["Object finalizee = this.get()"]引用 obj, 因此 第二次 System.gc 之後 obj 還沒有被回收
到這裏, Test09FinalReferentLifeCycle$FinalizedClazz 的實例 obj 在這樣的場景下多久被回收掉, 你應該知道了吧
4. 一些擴展
以下部分代碼, 截圖基於 openjdk9
另外 由於本人水平有限, 理解能力有限有限, 可能也會導致一些問題的存在
那麼對象怎麼註冊的 Finalizer 呢 ?
以如下參數 調試啓動 vm
-da -dsa -Xint -XX:+UseSerialGC -XX:+TraceFinalizerRegistration -XX:-RegisterFinalizersAtInit com.hx.test02.Test09FinalReferentLifeCycle
在 instanceKlass.register_finalizer 打一個斷點, 跑到我們關注的位置[i 爲Test09FinalReferentLifeCycle$FinalizedClazz 的實例 obj], 截圖如下
我們可以發現, 這裏 ident 爲 NULL, 也就是創建了對象, 還未執行構造方法 <init>, 然後 調用了 Finalizer.register, 參數爲 obj 的引用
完