欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

46 FinalReference.referent的回收时机

程序员文章站 2024-02-11 19:35:34
...

前言 

在某本书上面曾经看到过, Hotspot VM 的 gc 是准确式GC, 我的理解就是 这一次 gc 之后 应该会把所有的 "垃圾对象" 清理掉 

假设对应的 $FinalizedClazz 重写了 finalize 方法, 并且有一个 没有任何引用的实例 o, 那么 在下一次 gc 的时候应该回收掉对象 o, 但是 自从看了这部分的代码, 以及 结合一些调试工具, 调试代码, 似乎 发现实际情况 和我理解的是不一样的 

那么 这种情况下 o 多久会被回收呢 ?

 

测试代码如下 : 

package com.hx.test10;

/**
 * FinalReferentLifeCycle
 *
 * @author Jerry.X.He <aaa@qq.com>
 * @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 <aaa@qq.com>
     * @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 的所有实例, 如下图所示 

46 FinalReference.referent的回收时机

46 FinalReference.referent的回收时机

我们可以发现 给定的 vm 里面仅仅只有一个 Test09FinalReferentLifeCycle$FinalizedClazz 的实例, 那么即为 我们 main 方法里面 new 的这一个 Test09FinalReferentLifeCycle$FinalizedClazz 的实例 obj 

1.5 放开断点运行到第二个System.gc, 也是第一个 System.gc 之后, 再来查询 Test09FinalReferentLifeCycle$FinalizedClazz 的所有实例, 如下图所示 

46 FinalReference.referent的回收时机

我们可以发现 第一次 System.gc 之后, 这个 Test09FinalReferentLifeCycle$FinalizedClazz 的实例 obj 依然可以找到, 也就是 还没有被 gc 掉, 并且下面 输出了 obj.finalize 的相关日志, "do finalize, ident : randomString", 表示 该对象的 finalize 的阶段已经完成 

1.6 继续往下看, 走到第二次 System.gc 之后, 再来查询 Test09FinalReferentLifeCycle$FinalizedClazz 的所有实例, 如下图所示 

46 FinalReference.referent的回收时机

可以看到 第二次 System.gc 之后, 这个 Test09FinalReferentLifeCycle$FinalizedClazz 的实例 obj 已经找不到了, 也就是 被 gc 掉了 

 

2. 基于HSDB的调试 - 挂起FinalizedClazz.finalize

我们直接跳到重点, 第二次 System.gc 之前的情况均是一致的 

2.1 走到第二次 System.gc 之后, 再来查询 Test09FinalReferentLifeCycle$FinalizedClazz 的所有实例, 如下图所示 

46 FinalReference.referent的回收时机

可以看到 第二次 System.gc 之后, 这个 Test09FinalReferentLifeCycle$FinalizedClazz 的实例  obj 依然能够被找到, 也就是 还没有被 gc 掉

 

2.2 那么 为什么呢 ?, 首先我们看一下 引用 obj 的地方吧 

我们来看看什么对象 在引用我们的 Test09FinalReferentLifeCycle$FinalizedClazz 的实例  obj 吧, 点击 compute liveness 

46 FinalReference.referent的回收时机

这里的引用来自于 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;

46 FinalReference.referent的回收时机

在 Test09FinalReferentLifeCycle$FinalizedClazz.finalize 里面打一个断点 

46 FinalReference.referent的回收时机

 

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], 截图如下 

46 FinalReference.referent的回收时机

我们可以发现, 这里 ident 为 NULL, 也就是创建了对象, 还未执行构造方法 <init>, 然后 调用了 Finalizer.register, 参数为 obj 的引用 

 

 

完 

 

相关标签: java reference