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

彻底理解 FinalReference与Finalizer原创

722423

摘要

之前写了一篇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,但如果我们要实现资源回收这种功能该如何实现呢?

请先登录,再评论

"通常JVM在GC时如果发现一个对象只有对应的Reference引用就会将其对应的Reference对象加入到对应的pending-reference链中", "一旦重写了finalize方法就无法保证其一定会在某次GC前一定能执行完,这样引用的对象只能在下次或者是后面GC时才会回收", 有些地方感觉描述的不够清晰

1年前

美团大佬写的那篇文章链接能贴一下吗?网络上搜不到,搜出来的不知道哪个是美团大佬写的

1年前

为你推荐

不起眼,但是足以让你有收获的JVM内存分析案例
分析 这个问题说白了,就是说有些int[]对象不知道是哪里来的,于是我拿他的例子跑了跑,好像还真有这么回事。点该 dump 文件详情,查看相关的 int[] 数组,点该对象的“被引用对象”,发现所
从一起GC血案谈到反射原理
前言 首先回答一下提问者的问题。这主要是由于存在大量反射而产生的临时类加载器和 ASM 临时生成的类,这些类会被保留在 Metaspace,一旦 Metaspace 即将满的时候,就会触发 Fu
关于内存溢出,咱再聊点有意思的?
概述 上篇文章讲了JVM在GC上的一个设计缺陷,揪出一个导致GC慢慢变长的JVM设计缺陷,可能有不少人还是没怎么看明白的,今天准备讲的大家应该都很容易看明白 本文其实很犹豫写不写,因为感觉没有
协助美团kafka团队定位到的一个JVM Crash问题
概述 有挺长一段时间没写技术文章了,正好这两天美团kafka团队有位小伙伴加了我微信,然后咨询了一个JVM crash的问题,大家对crash的问题都比较无奈,因为没有现场,信息量不多,碰到这类问题我
又发现一个导致JVM物理内存消耗大的Bug(已提交Patch)
概述 最近我们公司在帮一个客户查一个JVM的问题(JDK1.8.0_191-b12),发现一个系统老是被OS Kill掉,是内存泄露导致的。在查的过程中,阴差阳错地发现了JVM另外的一个Bug。这个B
JVM实战:优化我的IDEA GC
IDEA是个好东西,可以说是地球上最好的Java开发工具,但是偶尔也会卡顿,仔细想想IDEA也是Java开发的,会不会和GC有关,于是就有了接下来对IDEA的GC进行调优 IDEA默认JVM参数: -
不起眼,但是足以让你收获的JVM内存案例
今天的这个案例我觉得应该会让你涨姿势吧,不管你对JVM有多熟悉,看到这篇文章,应该还是会有点小惊讶的,不过我觉得这个案例我分享出来,是想表达不管多么奇怪的现象请一定要追究下去,会让你慢慢变得强大起来,
如何通过反射获得方法的真实参数名(以及扩展研究)
前段时间,在做一个小的工程时,遇到了需要通过反射获得方法真实参数名的场景,在这里我遇到了一些小小的问题,后来在部门老大的指导下,我解决了这个问题。通过解决这个问题,附带着我了解到了很多新的知识,我觉得