性能文章>彻底理解 FinalReference与Finalizer>

彻底理解 FinalReference与Finalizer原创

782823

摘要

之前写了一篇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一直存在。

image.png

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

初识FinalReference与Finalizer

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

  • class FinalReference<T> extends Reference<T> {
  • public FinalReference(T referent, ReferenceQueue<? super T> q) {
  • super(referent, q);
  • }
  • }
  •  

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

  • final class Finalizer extends FinalReference<Object> {
  • //存放Finalizer的引用队列
  • private static ReferenceQueue<Object> 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中取出元素做进一步的清理工作。

image.png

同样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.currentThread().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 相关)

总结

本文分析了Finalizer的源码,并给出了"为什么如果在执行Object#finalize方法时当前对象又被重新赋值,那下次GC就不会再执行finalize方法了?"的答案。希望对大家有所帮忙。文章不正确处还望指正,同时欢迎关注个人技术公众号 洞悉源码,后序源源不断地给大家分享各类干货。最后再抛出一下问题给大家,JDK9中已将Object#finalize方法标志为Deprecated,但如果我们要实现资源回收这种功能该如何实现呢?

点赞收藏
叶易_公众号洞悉源码
请先登录,查看2条精彩评论吧
快去登录吧,你将获得
  • 浏览更多精彩评论
  • 和开发者讨论交流,共同进步

为你推荐

【全网首发】一次想不到的 Bootstrap 类加载器带来的 Native 内存泄露分析

【全网首发】一次想不到的 Bootstrap 类加载器带来的 Native 内存泄露分析

记一次“雪花算法”造成的生产事故的排查记录

记一次“雪花算法”造成的生产事故的排查记录

【全网首发】一次疑似 JVM Native 内存泄露的问题分析

【全网首发】一次疑似 JVM Native 内存泄露的问题分析

解读JVM级别本地缓存Caffeine青出于蓝的要诀2 —— 弄清楚Caffeine的同步、异步回源方式

解读JVM级别本地缓存Caffeine青出于蓝的要诀2 —— 弄清楚Caffeine的同步、异步回源方式

单服务并发出票实践

单服务并发出票实践

刺激,线程池的一个BUG直接把CPU干到100%了。

刺激,线程池的一个BUG直接把CPU干到100%了。

3
2