前言
在某本书上面曾经看到过, 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 的引用
完