FinalReference 如何使 GC 过程变得拖拖拉拉
本文基于 OpenJDK17 进行讨论,垃圾回收器为 ZGC。
提示: 为了方便大家索引,特将在上篇文章 《以 ZGC 为例,谈一谈 JVM 是如何实现 Reference 语义的》 中讨论的众多主题独立出来。
FinalReference 对于我们来说是一种比较陌生的 Reference 类型,因为我们好像在各大中间件以及 JDK 中并没有见过它的应用场景,事实上,FinalReference 被设计出来的目的也不是给我们用的,而是给 JVM 用的,它和 Java 对象的 finalize()
方法执行机制有关。
我们看到 finalize()
方法在 OpenJDK9 中已经被标记为 @Deprecated
了,并不推荐使用。笔者其实一开始也并不想提及它,但是思来想去,本文是主要介绍各类 Refernce 语义实现的,前面笔者已经非常详细的介绍了 SoftReference,WeakReference,PhantomReference 在 JVM 中的实现。
在文章的最后何不利用这个 FinalReference 将前面介绍的内容再次为大家串联一遍,加深一下大家对 Reference 整个处理链路的理解,基于这个目的,才有了本小节的内容。但笔者的本意并不是为了让大家使用它。
下面我们还是按照老规矩,继续从 JDK 以及 JVM 这两个视角全方位的介绍一下 FinalReference 的实现机制,并为大家解释一下这个 FinalReference 如何使整个 GC 过程变得拖拖拉拉,磨磨唧唧~~~
1. 从 JDK 视角看 FinalReference
FinalReference 本质上来说它也是一个 Reference,所以它的基本语义和 WeakReference 保持一致,JVM 在 GC 阶段对它的整体处理流程和 WeakReference 也是大致一样的。
唯一一点不同的是,由于 FinalReference 是和被它引用的 referent 对象的 finalize()
执行有关,当一个普通的 Java 对象在整个 JVM 堆中只有 FinalReference 引用它的时候,按照 WeakReference 的基础语义来讲,这个 Java 对象就要被回收了。
但是在这个 Java 对象被回收之前,JVM 需要保证它的 finalize()
被执行到,所以 FinalReference 会再次将这个 Java 对象重新标记为 alive,也就是在 GC 阶段重新复活这个 Java 对象。
后面的流程就和其他 Reference 一样了,FinalReference 也会被 JVM 加入到 _reference_pending_list 链表中,ReferenceHandler 线程被唤醒,随后将这个 FinalReference 从 _reference_pending_list 上摘下,并加入到与其关联的 ReferenceQueue 中,这个流程就是我们第三小节主要讨论的内容,大家还记得吗 ?
和 Cleaner 不同的是,对于 FinalReference 来说,在 JDK 中还有一个叫做 FinalizerThread
线程来专门处理它,FinalizerThread
线程会不断的从与 FinalReference 关联的 ReferenceQueue 中,将所有需要被处理的 FinalReference 摘下,然后挨个执行被它所引用的 referent 对象的 finalize()
方法。
随后在下一轮的 GC 中,FinalReference 对象以及它引用的 referent 对象才会被 GC 回收掉。
以上就是 FinalReference 被 JVM 处理的整个生命周期,下面让我们先回到最初的起点,这个 FinalReference 是怎么和一个 Java 对象关联起来的呢 ?
我们知道 FinalReference 是和 Java 对象的 finalize()
方法执行有关的,如果一个 Java 类没有重写 finalize()
方法,那么在创建这个 Java 类的实例对象的时候将不会和这个 FinalReference 有任何的瓜葛,它就是一个普通的 Java 对象。
但是如何一个 Java 类重写了 finalize()
方法 ,那么在创建这个 Java 类的实例对象的时候, JVM 就会将一个 FinalReference 实例和这个 Java 对象关联起来。
我们看到,在 JVM 创建对象实例的时候,会首先通过 has_finalizer()
方法判断这个 Java 类有没有重写 finalize()
方法,如果重写了就会调用 register_finalizer
方法,JVM 最终会调用 JDK 中的 Finalizer 类的静态方法 register。
在这里 JVM 会将刚刚创建出来的普通 Java 对象 —— finalizee,与一个 Finalizer 对象关联起来, Finalizer 对象的类型正是 FinalReference 。这里我们可以看到,当一个 Java 类重写了 finalize()
方法的时候,每当创建一个该类的实例对象,JVM 就会自动创建一个对应的 Finalizer 对象。
Finalizer 的整体设计和之前介绍的 Cleaner 非常相似,不同的是 Cleaner 是一个 PhantomReference,而 Finalizer 是一个 FinalReference。
它们都有一个 ReferenceQueue,只不过 Cleaner 中的那个基本没啥用,但是 Finalizer 中的这个 ReferenceQueue 却有非常重要的作用。
它们内部都有一个双向链表,里面包含了 JVM 堆中所有的 Finalizer 对象,用来确保这些 Finalizer 在执行 finalizee 对象的 finalize()
方法之前不会被 GC 回收掉。
在创建 Finalizer 对象的时候,首先会调用父类方法,将被引用的 Java 对象以及 ReferenceQueue 关联注册到 FinalReference 中。
最后将这个 Finalizer 对象插入到双向链表 —— unfinalized 中。
这个结构是不是和第三小节中我们介绍的 Cleaner 非常相似。
而 Cleaner 最后是被 ReferenceHandler 线程执行的,那这个 Finalizer 最后是被哪个线程执行的呢 ?
这里就要引入另一个 system thread 了,在 Finalizer 类初始化的时候会创建一个叫做 FinalizerThread 的线程。
FinalizerThread 的优先级被设置为 Thread.MAX_PRIORITY - 2
,还记得 ReferenceHandler 线程的优先级吗 ?
而一个普通的 Java 线程,它的默认优先级是多少呢 ?
我们可以看出这三类线程的调度优先级为:ReferenceHandler > FinalizerThread > Java 业务 Thead
。
FinalizerThread 线程在运行起来之后,会不停的从一个 queue 中获取 Finalizer 对象,然后执行 Finalizer 中的 runFinalizer 方法,这个逻辑是不是和 ReferenceHandler 线程不停的从 _reference_pending_list 中获取 Cleaner 对象,然后执行 Cleaner 的 clean 方法非常相似。
这个 queue 就是 Finalizer 中定义的 ReferenceQueue,在 JVM 创建 Finalizer 对象的时候,会将重写了 finalize()
方法的 Java 对象与这个 ReferenceQueue 一起注册到 FinalReference 中。
那这个 ReferenceQueue 中的 Finalizer 对象是从哪里添加进来的呢 ?这就又和我们第三小节中介绍的内容遥相呼应起来了,就是 ReferenceHandler 线程添加进来的。
当一个 Java 对象在 JVM 堆中只有 Finalizer 对象引用,除此之外没有任何强引用或者软引用之后,JVM 首先会将这个 Java 对象复活,在本次 GC 中并不会回收它,随后会将这个 Finalizer 对象插入到 JVM 内部的 _reference_pending_list 中,然后从 waitForReferencePendingList()
方法上唤醒 ReferenceHandler 线程。
ReferenceHandler 线程将 _reference_pending_list 中的 Reference 对象挨个摘下,注意 _reference_pending_list 中保存的既有 Cleaner,也有其他的 PhantomReference,WeakReference,SoftReference,当然也有本小节的 Finalizer 对象。
如果摘下的是 Cleaner 对象那么就执行它的 clean 方法,如果是其他 Reference 对象,比如这里的 Finalizer,那么就通过 ref.enqueueFromPending()
,将这个 Finalizer 对象插入到它的 ReferenceQueue 中。
当这个 ReferenceQueue 有了 Finalizer 对象之后,FinalizerThread 线程就会被唤醒,然后执行 Finalizer 对象的 runFinalizer 方法。
Finalizer 的内部有一个双向链表 —— unfinalized,它保存了当前 JVM 堆中所有的 Finalizer 对象,目的是为了避免在执行其引用的 referent 对象的 finalize()
方法之前被 GC 掉。
在 runFinalizer 方法中首先要做的就是将这个 Finalizer 对象从双向链表 unfinalized 上摘下,然后执行 referent 对象的 finalize()
方法。这里我们可以看到,大家在 Java 类中重写的 finalize()
方法就是在这里被执行的。
最后调用 Finalizer 对象(FinalReference 类型)的 clear 方法,将其引用的 referent 对象置为 null , 在下一轮 GC 的时候, 这个 Finalizer 对象以及它的 referent 对象就会被 GC 掉。
2. 从 JVM 视角看 FinalReference
现在我们已经从 JVM 的外围熟悉了 JDK 处理 FinalReference 的整个流程,本小节,笔者将继续带着大家深入到 JVM 的内部,看看在 GC 的时候,JVM 是如何处理 FinalReference 的。
在本文 5.1 小节中,笔者为大家介绍了 ZGC 在 Concurrent Mark 阶段如何处理 Reference 的整个流程,只不过当时我们偏重于 Reference 基础语义的实现,还未涉及到 FinalReference 的处理。
但我们在明白了 Reference 基础语义的基础之上,再来看 FinalReference 的语义实现就很简单了,总体流程是一样的,只不过在一些地方做了些特殊的处理。
在 ZGC 的 Concurrent Mark 阶段,当 GC 线程遍历标记到一个 FinalReference 对象的时候,首先会通过 should_discover
方法来判断是否应该将这个 FinalReference 对象插入到 _discovered_list 中。判断逻辑如下:
首先获取这个 FinalReference 对象所引用的 referent 对象,如果这个 referent 对象在 JVM 堆中已经没有任何强引用或者软引用了,那么就会将 FinalReference 对象插入到 _discovered_list 中。
但是在插入之前还要通过 is_inactive
方法判断一下这个 FinalReference 对象是否在上一轮 GC 中被处理过了,
对于 FinalReference 来说,inactive 的标志是它的 next 字段不为空。
这里的 next 字段是干嘛的呢 ?比如说,这个 FinalReference 对象在上一轮的 GC 中已经被处理过了,那么在发生本轮 GC 之前,ReferenceHandler 线程就已经将这个 FinalReference 插入到一个 ReferenceQueue 中,这个 ReferenceQueue 是哪来的呢 ?
正是上小节中我们介绍的,JVM 创建 Finalizer 对象的时候传入的这个 queue。
而 ReferenceQueue 中的 FinalReference 对象就是通过它的 next 字段链接起来的,当一个 FinalReference 对象被 ReferenceHandler 线程插入到 ReferenceQueue 中之后,它的 next 字段就不为空了,也就是说一个 FinalReference 对象一旦进入 ReferenceQueue,它的状态就变为 inactive 了。
那么在下一轮的 GC 中如果一个 FinalReference 对象的状态是 inactive,表示它已经被处理过了,那么就不在重复添加到 _discovered_list 中了。
如果一个 FinalReference 对象之前没有被处理过,并且它引用的 referent 对象当前也没有任何强引用或者软引用关联,那么是不是说明这个 referent 就该被回收了 ?想想 FinalReference 的语义是什么 ? 是不是就是在 referent 对象被回收之前还要调用它的 finalize()
方法 。
所以为了保证 referent 对象的 finalize()
方法得到调用,JVM 就会在 discover
方法中将其复活。随后会将 FinalReference 对象插入到 _discovered_list 中,这样在 GC 之后 ,FinalizerThread 就会调用 referent 对象的 finalize()
方法了,这里是不是和上一小节的内容呼应起来了。
那么 JVM 如何将一个被 FinalReference 引用的 referent 对象复活呢 ?
其实很简单,首先通过 ZPage::mark_object
将 referent 对应在标记位图 _livemap 的 bit 位标记为 1。其次调整 referent 对象的地址视图为 finalizable,表示该对象在回收阶段被 FinalReference 复活。
到现在 FinalReference 对象已经被加入到 _discovered_list 中了,referent 对象也被复活了,随后在 ZGC 的 Concurrent Process Non-Strong References 阶段,JVM 就会将 _discovered_list 中的所有 Reference 对象(包括这里的 FinalReference)统统转移到 _reference_pending_list 中,并唤醒 ReferenceHandler 线程去处理。
随后 ReferenceHandler 线程将 _reference_pending_list 中的 FinalReference 对象在添加到 Finalizer 中的 ReferenceQueue 中。随即 FinalizerThread 线程就会被唤醒,然后执行 Finalizer 对象的 runFinalizer 方法,最终就会执行到 referent 对象的 finalize() 方法。这是不是就和上一小节中的内容串起来了。
当 referent 对象的 finalize() 方法被 FinalizerThread 执行完之后,下一轮 GC 的这时候,这个 referent 对象以及与它关联的 FinalReference 对象就会一起被 GC 回收了。
总结
从整个 JVM 对于 FinalReference 的处理过程可以看出,只要我们在一个 Java 类中重写了 finalize() 方法,那么当这个 Java 类对应的实例可以被回收的时候,它的 finalize() 方法是一定会被调用的。
调用的时机取决于 FinalizerThread 线程什么时候被 OS 调度到,但是从另外一个侧面也可以看出,由于 FinalReference 的影响,一个原本该被回收的对象,在 GC 的过程又会被 JVM 复活。而只有当这个对象的 finalize() 方法被调用之后,该对象以及与它关联的 FinalReference 只能等到下一轮 GC 的时候才能被回收。
如果 finalize() 方法执行的很久又或者是 FinalizerThread 没有被 OS 调度到,这中间可能已经发生好几轮 GC 了,那么在这几轮 GC 中,FinalReference 和他的 referent 对象就一直不会被回收,表现的现象就是 JVM 堆中存在大量的 Finalizer 对象。
版权声明: 本文为 InfoQ 作者【bin的技术小屋】的原创文章。
原文链接:【http://xie.infoq.cn/article/04ddb39babf47bb7040464587】。
本文遵守【CC-BY 4.0】协议,转载请保留原文出处及本版权声明。
评论