Java ~ Reference ~ Finalizer【总结】

前言


 文章

一 概述


 简介

    Finalizer(终结者)类是FinalReference(终引用)类的唯一子类,同时也是Finalization(终结)机制的具体定义及实现者。所谓的终结机制本质是finalize()方法的调用机制,即finalize()方法是如何实现在F类对象被GC回收之前调用的。Java在终结者类中实现了终结机制的流程及结构,通过对终结者类的研究,我们可以掌握终结机制的绝大部分内容,因此直接将终结者类视作终结机制本身也未尝不可,该知识点会在下文详述。

    终结者类必须搭配引用队列使用,并且所有终结者都会固定将f-queue作为自身的注册引用队列。f-queue是在终结者类中创建的全局唯一引用队列,直接作用是承载所有的终结者,而其根本作用有二:一是追踪所指对象/F类对象的GC状态;二是作为JVM执行终结机制的起点,该知识点会在下文详述。

    当F类对象被创建时,JVM会自动将之作为所指对象创建终结者。终结者类对开发者直接屏蔽,即开发者无法直接创建终结者,JVM会在F类对象创建时自动为之创建终结者以进行终结机制。所谓F类,是指finalize()方法被重写且方法体不为空的类,因此虽然所有的类都存在finalize()方法(因为所有的类都继承自Object(对象)类),但JVM并不会为所有的对象创建终结者,同理也并非所有对象的finalize()方法都会被执行。

 结构

在这里插入图片描述

二 使用


    实际上,讲述终结者类的使用其实并没有太大意义,因为终结者类不是公共类,无法被其本身所在包外的类访问,故而开发者并无法使用终结者类。但为了清楚了解终结者类的相应功能/结构,此处我们还是会简单的介绍一下其关键的几个方法。

 创建

    终结者类没有公共的构造方法,具体的实例通过其提供的工厂方法创建。

  • static void register(Object finalizee) —— 创建固定将f-queue作为注册引用队列的终结者,并默认将终结者以头插法加入未终结链表中。该方法会由JVM在F类对象创建时调用,以创建将F类对象作为所指对象的终结者。
        方法会直接调用私有构造方法Finalizer(Object finalizee)实现。其首先固定将f-queue作为当前终结者的注册引用队列,随后调用add()方法将当前终结者加入未终结引用队列中。令人比较疑惑的是:从上述流程来看,似乎没有使用工厂方法register(Object finalizee)创建终结者的必要,因为如果只是为了使创建的终结者存在于未终结链表中的话,直接使用构造方法也是可以做到的。
/**
 * @Description: 注册终结者(即创建一个终结者并加入终结者队列,该方法由JVM进行调用)
 */
/* Invoked by VM */
static void register(Object finalizee) {
    new Finalizer(finalizee);
}

private Finalizer(Object finalizee) {
    // 采用一个静态的引用队列作为终结者引用队列,因此可知所有的终结者注册的都是同一个引用队列。
    super(finalizee, queue);
    // 将终结者对象加入终结者队列中。
    add();
}

 方法

  • private void add() —— 新增 —— 将当前终结者以头插法加入未终结链表中。该方法会在终结者创建时被调用,因此所有的终结者初始时都存在于未终结链表中。

  • private void remove() —— 移除 —— 将当前终结者从未终结链表中移除,该方法会在当前终结者终结后调用。

  • private boolean hasBeenFinalized() —— 是否终结 —— 判断当前终结者是否终结,即其所指对象/F类对象是否已执行finalize()方法,是则返回true;否则返回false。
        方法本质是判断终结者是否已从未终结链表中移除。上文已经说过,remove()方法只会在终结者终结之后被调用,因此终结者是否已从未终结链表中移除可变相作为终结者是否终结的判断依据。即如果终结者已从未终结链表中移除,则其必然已终结。

三 实现


 f-queue

    f-queue的本质其实就是在终结者类中创建的全局唯一引用队列,由于其在终结机制中发挥了重要作用,因此在JVM相关书籍中将之命名为了f-queue。f-queue的直接作用是承载所有的终结者。终结者类与PhantomReference(虚引用)类一样都必须搭配引用对象使用,而终结者创建时都会固定将f-queue作为其注册引用队列,因此最终所有的终结者都会因为Reference(引用)机制加入f-queue中。f-queue的根本作用有二:一是追踪所指对象/F类对象的GC状态,即判断所指对象/F类对象是否已/会被GC回收,这是引用队列的基本功能,此处不在赘述;二是作为JVM执行终结机制的起点,即JVM会从f-queue中取出终结者以执行其所指对象/F类对象的finalize()方法。

 未终结链表

在这里插入图片描述

    未终结链表的作用是建立终结者与GC ROOTS的关联,以确保终结者不会在终结机制期间被GC回收,这与Cleaner(清洁工)类中持有的清洁工链表作用相同。由于终结者由JVM创建,因此初始情况下,如果没有未终结链表,则终结者处于未与GC ROOTS建立直接/间接关联的状态,这就可能出现终结者的所指对象/F类对象尚未执行finalize()方法(更准确的说是终结者加入待定列表前,因为待定链表也是全局静态变量,属于GC ROOTS的范畴,但发现列表不是)就被GC回收的情况,导致部分终结者无法终结。因此为了避免这一点,JVM必须建立所有终结者与GC ROOTS的直接/间接关联,而未终结链表起的就是这个作用。

    未终结链表是由终结者类内部持有的全局唯一双向链表,即未终结链表是一个全局静态变量。未终结链表是逻辑链表,即未终结链表并不是一个类似于LinkedList(链接列表)类对象的对象。终结者类中用于持有未终结链表的[unfinalized @ 未终结]实际上持有的是未终结链表头终结者/节点的引用,由于每个终结者都持有着其在未终结链表中的前驱/后继终结者/节点,因此只要持有了头终结者/节点就相当于持有了整个未终结链表。未终结链表是线程安全的,终结者类通过synchronized关键字搭配静态对象的方式实现了类锁来保证未终结链表的线程安全。

    终结者会在其创建时默认加入未终结链表,因此所有的终结者初始时都存在于未终结链表中。关于上述内容粗略的说法是:当F类对象创建时,JVM会自动将之作为所指对象调用终结者类的register()方法创建终结者并加入未终结链表中。但实际上,关于终结者的加入时间还能有更精确的描述/调整。众所周知Java对象的创建并非原子操作,大致可以分为实例化(内存分配)与初始化(构造方法执行)两个部分。通过对JVM参数-XX:+RegisterFinalizersAtInit的调整 ,可以设置终结者在F类对象实例化后、初始化前加入,也可以设置其在F类对象初始化后加入。

void Heap::AddFinalizerReference(Thread* self, ObjPtr<mirror::Object>* object) {
    ScopedObjectAccess soa(self);
    ScopedLocalRef<jobject> arg(self->GetJniEnv(), soa.AddLocalReference<jobject>(*object));
    jvalue args[1];
    args[0].l = arg.get();
    // 调用 Java 层静态方法 FinalizerReference#add
    InvokeWithJValues(soa, nullptr, WellKnownClasses::java_lang_ref_FinalizerReference_add, args);
    *object = soa.Decode<mirror::Object>(arg.get());
}

    JVM是如何做到在指定的时间段里调用终结者类的register()方法的呢?这个就要根据不同的情况判断。针对在实例化后、初始化前调用的F类对象,由于内存分配是统一由内存管理系统负责的,因此可以由其负责处理;而对于在初始化后的F类对象则相对复杂,难道要在所有的构造方法尾部都进行处理吗?当然是不可能。实际上JVM使用了一个很灵活的方式,在Java中,子类的构造方法会调用父类的构造方法,因此最终都会执行Object(对象)类的构造方法。JVM将对象类构造方法里的return指令替换为_return_register_finalizer指令,从而在侵入性很小的情况下完成了方法调用。

    将终结者从未终结链表中移除无需遍历。由于未终结链表是双向链表,因此将终结者从未终结链表中移除并不需要遍历,因为终结者自身组合了字段用于保存其在未终结链表中的前驱/后继终结者/节点,因此直接在类锁的保护下将前驱/后继终结者/节点重新链接即可。终结者被移除后,其前驱/后继引用会指向自身,即自引用,作为其已从未终结链表中移除的标志。之所以不使用null作为标志是因为null已经被作为了头/尾终结者/节点的标志,因此可通过判断终结者是否自引用来判断终结者是否已被移除。由于终结者只会在其所指对象/F类对象的finalize()方法执行后才会被JVM从未终结链表中移除,因此其也可以变相作为终结者是否终结的判断依据,而事实上终结者类也确实是这么做的。

 终结者线程

    终结者线程是终结者类自实现内部类FinalizerThread(终结者线程)类的唯一实例,专用于执行终结机制。终结者线程类是Thread(线程)类的子类,因此其实例同样也是一个线程。终结者线程在终结者类的静态块中创建,是一个优先级为8的守护线程,因此整个JVM中只有一条终结者线程。很多资料都会很简略的介绍说终结机制的执行线程的优先级很低,容易造成终结者/F类对象堆积而导致OOM,但实际上这种说法并不准确,因为相对于大量优先级为5的用户线程来说,优先级为8的终结者线程优先级并不算低。导致OOM的原因更多是因为终结者线程只有一条,难以与数量众多的用户线程竞争CPU资源。

    终结者线程用于执行终结机制,简单的说就是不断的将终结者从f-queue及未终结链表中获取/移除终结者,并执行其所指对象/F类对象的finalize()方法,最后断开两者间的引用,以加速两者被GC回收,该知识点会在下文详述。

四 终结机制


在这里插入图片描述

 简介

    终结机制的本质是finalize()方法的调用机制,即finalize()方法是如何实现在F类对象被GC回收之前调用的。Java在终结者类中实现了终结机制的流程及结构,通过对终结者类的研究,我们可以掌握终结机制的绝大部分内容。虽然没有硬性规定,但终结机制最适合用于资源的释放/回收。由于finalize()方法会在F类象被GC回收前执行,因此可以趁此机会执行一些自定义操作。但如果要说最合适,则是用于对资源进行释放,例如I/O流、网络连接等。资源最明显的特点是往往需要使用堆外内存,而由于堆外内存不受GC的管理,因此正常情况下如果不手动释放资源,堆外内存永远无法被回收(也有不正常的情况,例如堆内存溢出导致Full GC时也会顺势回收)。基于该原因,终结机制往往会被用来作为资源释放的保底机制,通过在资源类的finalize()方法中重写资源释放的代码,可使得即使没有手动的释放,也可以在资源类对象被GC回收时触发释放。

    虽然终结机制确实提供了一套关于某些问题的处理方案,但并不推荐去使用它,因为其是非常不稳定的。正是因为终结机制如此的不靠谱,因此其只能在必要的情况下作为保底机制使用,而不能作为处理问题的常规方案。终结机制可能存在的问题如下:

  • 终结线程的优先级相对较低,finalize()方法的执行速度可能小于终结者/F类对象新增的速度,使得终结者/F类对象累积而导致新生代频繁GC -> 老年代频繁GC -> FULL GC -> OOM;
  • 如果某个F类对象的finalize()方法执行时间很长,会使得终结者/F类对象累积而导致新生代频繁GC -> 老年代频繁GC -> FULL GC -> OOM;
  • JVM/程序运行时,只保证finalize()方法一定会被调用,但不保证会等待其执行结束;
  • JVM/程序退出时,无法保证累积的所有F类对象的finalize()方法都被执行,因此可能导致资源泄露。

 流程

    终结机制和引用机制的关系甚深。很难具体言明两者之间的关联,即可以认为终结机制是引用机制的一部分,也可以认为终结机制是引用机制的衍生,甚至还可以认为引用机制是终结机制的一部分。但无论是那种说法,终结机制依赖于引用机制实现都是毫无争议的事实,在囊括引用机制流程的情况下,终结机制的完整流程如下:

  • F类对象创建,JVM自动将之作为所指对象创建终结者。终结者会被默认加入未终结链表,并且终结者作为终引用也会被默认加入发现列表,故而终结者初始会同时存在于发现列表与未终结链表中,此时终结者处于活跃状态;
  • F类对象被GC判定为可回收,其终结者被GC线程从发现列表中移除并头插至待定列表(堆栈)中,此时终结者处于待定状态;
  • 引用处理器线程将终结者从待定列表(堆栈)中移除并头插至f-queue中,此时终结者处于入队状态;
  • 终结者线程将终结者从f-queue中移除,此时终结者处于怠惰状态;
  • 终结者线程继续将之从未终结链表中移除,随后执行其所指对象/F类对象的finalize()方法;
  • 当JVM判断F类对象的finalize()方法执行已/可结束后,终结者线程会断开终结者与其的关联,令两者可便于被GC回收(不是可被回收,即使不断开关联也是可以回收的)。

 辅助终结

    一方面为了避免终结者/F类对象累积过多而导致OOM,另一方面为了在JVM/程序退出时能尽可能保证所有F类对象的finalize()方法都被执行,终结机制存在加速/退出两种辅助终结操作(名字我自己瞎取的),会分别在上述两种情况下执行。虽说命名上有所差异,但实际上两种辅助终结操作的核心是一致的,即创建新的线程参与活动,以加速原本只由终结者线程进行的终结机制的执行效率。但其中需要注意的是:创建的辅助线程并非终结者线程类的实例,只是普通的用户线程,这些线程在名义上被称为二级终结者线程。

    当终结者/F类对象累积过多时,为了避免出现OOM,会触发加速辅助终结操作。该操作会创建二级终结者线程参与终结机制的执行,执行流程也与标准流程基本相同,唯一的区别在于当终结者线程发现f-queue为空时会进行无限等待状态,直至有新终结者入队被唤醒。而当二级终结者线程发现f-queue为空时会直接结束,即二级终结者线程会直接退出/消亡。说的更加通俗一点,即终结者线程从f-queue中移除/获取终结者使用的是引用队列类的remove()方法,而二级终结者线程使用的则是poll()方法。

    当JVM/程序退出时,为了尽可能保证所有F类对象的finalize()方法都被执行,会触发退出辅助终结操作。退出辅助终结操作同样会创建二级终结者线程,但在执行流程上却与标准流程有较大差异。该差异体现在其不会再从f-queue中获取终结者,而是直接从未终结链表中获取终结者。这是因为当JVM/程序退出时,引用机制可能已经不再执行,即相关GC、引用处理器等执行线程可能已经消亡,因此终结者可能已无法再加入f-queue中。为了尽可能保证所有F类对象的finalize()方法都被执行,直接从未终结链表中获取未终结的终结者是最好的方式。由于在执行F类对象的finalize()方法前会判断其终结者是否已从未终结链表中移除,因此无需担心多线程执行时重复执行的情况。需要注意的是:由于JVM/程序退出时的剩余运行时刻不可控,因此虽然退出辅助终结操作会被触发,但同样无法保证所有F类对象的finalize()方法都被执行。

 其它

    F类对象的finalize()方法只能被终结机制执行一次。这是面试中经常会遇到的一个问题,之所以只能执行一次,是因为执行finalize()方法的终结者线程/二级终结者线程会从f-queue/未终结链表中获取终结者。而一旦F类对象的finalize()方法被执行,则终结者必然已从f-queue/未终结链表中移除,因此无法执行第二次。但注意,只能执行一次只是单纯基于终结机制而言的,而开发者完全可以手动且不限次数的执行finalize()方法。这里可能存在一个疑问,即finalize()方法执行后F类对象不是会被GC回收吗?怎么还会有执行第二次的说法呢?这是因为finalize()方法执行后F类对象未必一定会被GC回收,如果F类对象在finalize()方法中成功与GC ROOTS重新建立了直接/间接关联,则确实是可以避免自身被GC回收的。

    F类对象至少要两次GC才能被真正回收。第一次GC将F类对象判定为可回收并执行终结机制;第二次GC则会将执行完finalize()方法并断开与终结者关联的F类对象正式回收。而在这两次GC之间也可能存在有多次的GC,因此是至少两次。

五 JVM参数


  • -XX:+RegisterFinalizersAtInit —— 设置终结者加入未终结链表的时间。为false时在F类对象实例化后、初始化前加入,为true时在F类对象初始化后加入。默认true。
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

说淑人

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值