02 FinalReference.referent的回收時機

前言 

在某本書上面曾經看到過, 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 的引用 

 

 

完 

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章