性能文章>02 FinalReference.referent的回收时机>

02 FinalReference.referent的回收时机原创

1年前
300034

前言 

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

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

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

 

测试代码如下 : 

package com.hx.test10;

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

我们可以发现 给定的 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 的引用 

 

 

完 

 

点赞收藏
分类:标签:
黄金键盘
请先登录,查看3条精彩评论吧
快去登录吧,你将获得
  • 浏览更多精彩评论
  • 和开发者讨论交流,共同进步

为你推荐

随机一门技术分享之Netty

随机一门技术分享之Netty

从 Linux 内核角度探秘 JDK MappedByteBuffer

从 Linux 内核角度探秘 JDK MappedByteBuffer

MappedByteBuffer VS FileChannel:从内核层面对比两者的性能差异

MappedByteBuffer VS FileChannel:从内核层面对比两者的性能差异

4
3