finalize()-原理,了解一下?

前言

在之前深入浅出 JVM GC(1)我们知道,finalize 方法的作用是:

如果对象在进行可达性分析后发现没有与 GC Roots 相连接的引用链,那他将会被第一次标记并且进行一次筛选,筛选的条件是此对象是否有必要执行 finalize 方法。

注意:当对象没有覆盖 finalize 方法,或者 finalize 方法已经被虚拟机调用过,虚拟机将这两种情况都视为 “没有必要执行”。也就是说,finalize 方法只会被执行一次。

如果这个对象被判定为有必要执行 finalize 方法,那么这个对象将会放置在一个叫做 F-Queue 的队列之中,并在稍后由一个虚拟机自动建立的,优先级为 8 的 Finalizer 线程去执行它。

注意:如果一个对象在 finalize 方法中运行缓慢,将会导致队列后的其他对象永远等待,严重时将会导致系统崩溃。

finalize 方法是对象逃脱死亡命运的最后一道关卡。稍后 GC 将对队列中的对象进行第二次规模的标记,如果对象要在 finalize 中 “拯救” 自己,只需要将自己关联到引用上即可,通常是 this。
如果这个对象关联上了引用,那么在第二次标记的时候他将被移除出 “即将回收” 的集合;如果对象这时候还没有逃脱,那基本上就是真的被回收了。

那么,就看看这个方法的具体原理。

测试 demo

public class FinalizeTest {

public static void main(String[] args) {
FinalizeTest f = new FinalizeTest();
f = null;
System.gc();
}

@Override
protected void finalize() throws Throwable {
System.out.println("finalize");
}
}

这是我们的测试 demo,然后,我们在 finalize 方法中,打上断点。启动 JVM,得到以下堆栈。

image.png

可以看到,一个 FinalizerThread 的线程执行了我们的 finalize 方法。那么过程是如何的呢?

堆栈分析

这个 FinalizerThread 的初始化和启动在 Finalizer 的 static 块中,由 JVM 主动访问其外部类 Finalizer 初始化这个静态块。具体访问方法是 Finalizer 的 register 方法。

静态块会启动这个线程,这个线程的优先级是 8 ,比普通的线程要高一点,但是是 demon 线程。

这个线程的任务则是死循环从 Finalizer 的队列中,取出 Finalizer 对象,然后调用这些对象的 runFinalizer 方法。

而这个队列是一个 ReferenceQueue 队列 。里面存放的就是 Finalizer 对象,当一个对象需要执行 finalize 方法(未执行过且重写了该方法)的时候, JVM 会将这个对象包装成 Finalizer 实例,然后,链接到 Finalizer 链表中,并放入这个队列(详细的等会再讲)。

而这个 runFinalizer 方法的具体逻辑则是获取 Finalizer 对象包装的引用,即实际对象(是枚举则跳过),执行这个对象的 finalize 方法。执行完毕后,清空 Finalizer。

到这里,一个对象的 finalize 方法就执行结束了。

如何放入队列?

Finalizer 继承了 Reference 类,该类和 GC 密切相关。

而该类有一个高优先级的线程—— ReferenceHandler。他的任务则是死循环执行 tryHandlePending 方法。处理 Reference 的 pending 属性,而这个属性其实就是 Reference 自己。GC 的时候,会设置这个地址 pending 地址。这段代码在 Hotspot 中。有兴趣的可以看看。

当这个线程发现 pending 地址不是空,就会尝试将自身放到自己的 queue 属性队列中。

代码如下:

ReferenceQueue<? super Object> q = r.queue;
if (q != ReferenceQueue.NULL) q.enqueue(r);

因此,当我们构造了一个 Finalizer 对象,这个对象会被 GC 设置到自该对象的 pending 属性中,然后 ReferenceHandler 线程会处理这个 pending 属性,具体处理则是将自己添加到构造函数设置的队列中。

这个时候,Finalizer 中的线程就可以从队列中取出这个 Finalizer 对象了。

而这一切都是虚拟机做的。

总结

finalize 方法高度依赖 JVM 和 GC,当一个对象被标记后,便会被 JVM 包装成 Finalizer 对象,然后,被 JVM 设置到 Reference 的静态属性 pending 中,Reference 的内部线程则会将这个 pending 放入到构造函数的队列中。

Finalizer 的内部线程则会从队列中取出 Finalizer 对象,并调用其包装的实际对象的 finalize 方法。

所以,finalize 方法需要两个线程来处理他,一个是 ReferenceHandler ,一个是 FinalizerThread。

前者负责将 Finalizer 对象放入到 Reference 队列中,后者负责从队列中取出 Finalizer 对象并调用实际对象的 finalize 方法。

同时,GC 大概也要做 2 件事情,一个是创建 Finalizer 对象,一个是将该对象设置到自己的 pending 属性中。

拾遗

在 Reference 的 tryHandlePending 方法中,有一个需要注意的地方,就是 Cleaner,相关代码如下:

static boolean tryHandlePending(boolean waitForNotify) {
Reference<Object> r;
Cleaner c;
try {
synchronized (lock) {
if (pending != null) {
r = pending;
c = r instanceof Cleaner ? (Cleaner) r : null;
pending = r.discovered;
r.discovered = null;
} else {
if (waitForNotify) {
lock.wait();
}
// retry if waited
return waitForNotify;
}
}
} catch (OutOfMemoryError x) {
Thread.yield();
return true;
} catch (InterruptedException x) {
return true;
}
if (c != null) {
c.clean();
return true;
}

ReferenceQueue<? super Object> q = r.queue;
if (q != ReferenceQueue.NULL) q.enqueue(r);
return true;
}

判断如果,这个引用时 Cleaner 类型,执行该类的 clean 方法就可以了,就不放入队列了。而这个 Cleaner 和 NIO 的直接内存相关,这点其实在楼主分析 Netty 的 noCleaner 策略时提过。

DirectByteBuffer 类中有个 Deallocator 线程,该线程的 run 方法就是调用 unsafe.freeMemory(address) 方法释放直接内存。

当构造 DirectByteBuffer 对象的时候,会创建一个相应的 Deallocator。

而这个 Cleaner 对象则包装了这个 Deallocator,当调用 Cleaner 的 clean 方法的时候,实际上,调用的是用 Deallocator 的 run 方法。这样,当 Cleaner 对象回收的时候,就可以顺手清理直接内存。

由于 DirectByteBuffer 对象中的 Cleaner 目前除了自己使用外,无他人使用,那么当 DirectByteBuffer 被回收时,Cleaner 也会被回收,自然,也就会执行 Finalizer 的逻辑了。

注意:这个 Deallocator 线程只有一个构造方法会创建它 —— DirectByteBuffer(int cap). 对应的 ByteBuffer 构造方法应该是 static ByteBuffer allocateDirect(int capacity)

使用的时候需要注意。