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

 更新时间:2022年01月24日 10:13:08   作者:叶易_公众号洞悉源码  
这篇文章主要为大家介绍了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<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中取出元素做进一步的清理工作。

同样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,但如果我们要实现资源回收这种功能该如何实现呢?

相关文章

  • javaweb实现在线支付功能

    javaweb实现在线支付功能

    这篇文章主要为大家详细介绍了javaweb实现在线支付功能的相关资料,具有一定的参考价值,感兴趣的小伙伴们可以参考一下
    2016-11-11
  • JAVA文件扫描(递归)的实例代码

    JAVA文件扫描(递归)的实例代码

    这篇文章主要介绍了JAVA文件扫描(递归)的实例代码 ,代码简单易懂,非常不错,具有一定的参考借鉴价值,需要的朋友可以参考下
    2019-06-06
  • Java使用I/O流读取文件内容的方法详解

    Java使用I/O流读取文件内容的方法详解

    这篇文章主要介绍了Java使用I/O流读取文件内容的方法,结合实例形式详细分析了java使用I/O流读取文件常见操作技巧,需要的朋友可以参考下
    2019-11-11
  • 一文搞懂Spring循环依赖的原理

    一文搞懂Spring循环依赖的原理

    这篇文章将用实例来为大家详细介绍@Autowired解决循环依赖的原理,文中的示例代码讲解详细,对我们学习Spring有一定帮助,感兴趣的可以学习一下
    2022-07-07
  • 从java面试题了解你所模糊的数组

    从java面试题了解你所模糊的数组

    这篇文章主要介绍了从java面试题了解你所模糊的数组,数组用来存储一系列的数据项,其中的每一项具有相同的基本数据类型、类或相同的父类。通过使用数组,可以在很大程度上缩短和简化程序代码,从而提高应用程序的效率。,需要的朋友可以参考下
    2019-06-06
  • spring集成okhttp3的步骤详解

    spring集成okhttp3的步骤详解

    okhttp是一个封装URL,比HttpClient更友好易用的工具,下面这篇文章主要给大家介绍了关于spring集成okhttp3的相关资料,文中通过示例代码介绍的非常详细,对大家的学习或工作具有一定的参考学习价值,需要的朋友们下面来一起看看吧。
    2018-04-04
  • Java统计代码的执行时间的N种方法

    Java统计代码的执行时间的N种方法

    在日常开发中经常需要测试一些代码的执行时间,但又不想使用向 JMH(Java Microbenchmark Harness,Java 微基准测试套件)这么重的测试框架,所以本文就汇总了一些 Java 中比较常用的执行时间统计方法,总共包含以下 6 种,需要的朋友可以参考下
    2022-08-08
  • 如何用Java实现排列组合算法

    如何用Java实现排列组合算法

    本文主要介绍了如何用Java实现排列组合算法,对算法感兴趣的同学,可以参考一下,理解其原理,并且试验一下。
    2021-05-05
  • 详解Java实现拓扑排序算法

    详解Java实现拓扑排序算法

    拓扑排序,很多人都可能听说但是不了解的一种算法。或许很多人只知道它是图论的一种排序,至于干什么的不清楚。又或许很多人可能还会认为它是一种啥排序。而实质上它是对有向图的顶点排成一个线性序列
    2021-06-06
  • SpringBoot上传图片与视频不显示问题的解决方案

    SpringBoot上传图片与视频不显示问题的解决方案

    这篇文章主要介绍了关于springboot上传图片与视频不显示问题,最近做毕设时候需要上传视频的图片与视频,但是每次都需要重启前端才能展示出此图片,所以本文给大家介绍了SpringBoot上传图片与视频不显示问题的解决方案,需要的朋友可以参考下
    2024-03-03

最新评论