java编程FinalReference与Finalizer原理示例详解

网友投稿 377 2022-09-01


java编程FinalReference与Finalizer原理示例详解

之前写了一篇java编程Reference核心原理示例源码分析的文章,但由于篇幅和时间的原因没有给出FinalReference和Finalizer的分析。同时也没有说明为什么建议不要重写Object#finalize方法(实际上JDK9已经将Object#finalize方法标记为Deprecated)。将文章转发到perfma社区后,社区便有同学提出一个有意思的问题?"Object#finalize如果在执行的时候当前对象又被重新赋值,那下次GC就不会再执行finalize方法了,这是为什么啊” 。看到这个问题时我知道答案一定和Finalizer有关,于是便有了这篇幅文章。(ps:perfma社区有很多高质量的文章,同时里面有很多实用的工具JVM参数分析、Java线程dump分析、Java内存dump分析都有,感兴趣的同学可以关注一下。)

概述

java编程Reference核心原理示例源码分析一文中提到JDK中有SoftReference、WeakReference、PhantomReference以及FinalReference,但并没有细说FinalReference。最开始Java语言其实就有了finalizers的机制,然后才引用了特殊Reference机制,也就是SoftReference、WeakReference、PhantomReference以及FinalReference,通过他们来处理资源或内存回收的问题。FinalReference与Finalizer平时开发时是用不到,但你Debug、线程dump或者heap dump 分析时,是否注意到Finalizer一直存在。

这个Finalizer到底是用来干什么的?为什么建议不要重写Object#finalize方法?为什么如果在执行Object#finalize方法时当前对象又被重新赋值,那下次GC就不会再执行finalize方法了?本文将通过源码分析解释这些问题。

初识FinalReference与Finalizer

JDK中FinalReference在JDK里的实现如下:

class FinalReference extends Reference {

public FinalReference(T referent, ReferenceQueue super T> q) {

super(referent, q);

}

}

FinalReference实现很简单,可以说就是一个标记类,可以看到这个类访问权限为package,除了java.lang.ref包下面的类能引用其外其他类都无权限。Finalizer实现则相对复杂一点点。

final class Finalizer extends FinalReference {

//存放Finalizer的引用队列

private static ReferenceQueue queue = new ReferenceQueue<>();

//当前等待待执行Object#finalize方法的Finalizer节点

private static Finalizer unfinalized = null;

//锁对象

private static final Object lock = new Object();

//Finalizer链 后续节点与前驱节点

private Finalizer next = null, prev = null;

//私有构造函数

private Finalizer(Object finalizee) {

super(finalizee, queue);

//头插法将当前对象加入Finalizer链中

add();

}

/* Invoked by VM */

static void register(Object finalizee) {new Finalizer(finalizee);}

//头插法将当前对象加入Finalizer链中

private void add() {

//获取Finalizer类中全局锁对象对应moniter

synchronized (lock) {

if (unfinalized != null) {

this.next = unfinalized;

unfinalized.prev = this;

}

//更新等待待执行Object#finalize方法的节点

unfinalized = this;

}

}

}

从上面的JDK源码代码可以看到Finalizer对象实际是JVM通过调用Finalizer#register方法创建的,不通过反射我们是无法直接创建Finalizer对象的。Finalizer#register方法一方面创建了Finalizer对象,同时将创建的Finalizer对象加入到了Finalizer链中。实际上HotSpot实现上在创建一对象时,如果该类重写了Object#finalize方法且方法内容不为空,则会调Finalizer#register方法。

何时会调用类中重写的finalize方法

先看回顾一下上篇文章中最重的Reference核心处理流程。通常JVM在GC时如果发现一个对象只有对应的Reference引用就会将其对应的Reference对象加入到对应的pending-reference链中,同时会通知ReferenceHandler线程。ReferenceHandler线程收到通知后,如果对应的Reference对象不是Cleaner的实例,则会其将加入到ReferenceQueue队列中等待其他的线程去从ReferenceQueue中取出元素做进一步的清理工作。

同样Reference核心处理流程也适用于Finalizer(Finalizer的超类实际是Reference),而用于处理ReferenceQueue中Finalizer的线程是FinalizerThread。其是Finalizer内部的一个私有类,并且是一个守护线程。

private static class FinalizerThread extends Thread {

private volatile boolean running;

FinalizerThread(ThreadGroup g) {

//这个便是一面提到dump线程时会出现的Finalizer线程的名字

super(g, "Finalizer");

}

public void run() {

// 避免重复调用run方法

if (running)

return;

// Finalizer线程先于System.initializeSystemClass被调用。等待直到JavaLangAccess可以访问

while (!VM.isBooted()) {

try {

VM.awaitBooted();

} catch (InterruptedException x) {

// ignore and continue

}

}

final JavaLangAccess jla = SharedSecrets.getJavaLangAccess();

running = true;

//守护线程一直运行

for (;;) {

try {

//从ReferenceQueue中取出Finalizer

Finalizer f = (Finalizer)queue.remove();

//调用Finalizer引用对象重写的finalize方法,内部实现上会catch Throwable 异常,保证FinalizerThread线程一直能运行

f.runFinalizer(jla);

} catch (InterruptedException x) {

// ignore and continue

}

}

}

}

static {

ThreadGroup tg = Thread.currentThreahttp://d().getThreadGroup();

for (ThreadGroup tgn = tg;

tgn != null;

tg = tgn, tgn = tg.getParent());

Thread finalizer = new FinalizerThread(tg);

//线程优先级没有ReferenceHandler守护线程高

finalizer.setPriority(Thread.MAX_PRIORITY - 2);

//设置为守护线程

finalizer.setDaemon(true);

//启动线程

finalizer.start();

}

Finalizer#runFinalizer方法如下:

private void runFinalizer(JavaLangAccess jla) {

synchronized (this) {

//已从Finalizer链中摘除,则不再执行Finalizer引用的对象的finalize方法

if (hasBeenFinalized()) return;

remove();

}

try {

//获取Finalizer引用的对象

Object finalizee = this.get();

if (finalizee != null && !(finalizee instanceof java.lang.Enum)) {

/**JavaLangAccess实现内部会调用Finalizer引用的对象的finalize方法

* 实际是调用System#setJavaLangAccess方法实例化的JavaLangAccess对象

*/

jla.invokeFinalize(finalizee);

//清除栈中包含的该变量引用,以降低conservative GC 错误的保留该对象的机会

finalizee = null;

}

} catch (Throwable x) { }

super.clear();

}

问题答案

从上面Finalizer#runFinalizer方法源码可以看出一旦一个对象已从Finalizer链中摘除,则不再执行Finalizer引用的对象的finalize方法,即使在其finalize方法中再次强引用其本身。而另一个问题"为什么建议不要重写Object#finalize方法",一旦重写了finalize方法就无法保证其一定会在某次GC前一定能执行完,这样引用的对象只能在下次或者是后面GC时才会回收,这可能会出现内存泄露或是其它的GC问题。关于finalize引发的GC问题,感兴趣的同学可以看一下美团基础构架大佬写的 RPC采用短链接导致YoungGC耗时过长的问题分析与优化一文:一次 Young GC 的优化实践(FinalReference 相关)

总结


版权声明:本文内容由网络用户投稿,版权归原作者所有,本站不拥有其著作权,亦不承担相应法律责任。如果您发现本站中有涉嫌抄袭或描述失实的内容,请联系我们jiasou666@gmail.com 处理,核实后本网站将在24小时内删除侵权内容。

上一篇:Python爬虫技术--基础篇--IO编程(python io库)
下一篇:Python常用第三方库总结(python分析方向的第三方库)
相关文章

 发表评论

暂时没有评论,来抢沙发吧~