迁移到JDK18为何写一个空的finalize()方法?


现在JEP 421(Deprecate Finalization for Removal)已经在 J​​DK 18 中交付,似乎越来越多的人在谈论 finalization 以及如何迁移到替代方案,例如Cleaner. 

但是,为什么有人费尽心思写了一个空的finalize()方法,为什么它如此重要,以至于专门加有一个注释:警告不要删除它?

@SuppressWarnings("removal")
public void finalize() {
   
// DO NOT REMOVE THIS METHOD
}

答案是,一个空的finalize()会禁止对该类的所有实例和子类的所有实例进行finalize()(除非被子类重写)。根据该类的使用情况,这可能是一个重要的优化。

为了理解这一点,让我们回顾一下Java对象的生命周期:

一个没有finalize()的Java对象被创建,使用了一段时间后,最终变得无法访问。一段时间后,垃圾收集器注意到该对象是不可及的,并对其进行垃圾收集。

一个有finalize()的对象被创建,被使用了一段时间,最终变得不可触及。一段时间后,该对象的finalize()方法被运行。这是普通的Java代码,所以该对象现在实际上是可以访问的。
又过了一段时间,该对象再次变得不可触及,这一次,垃圾收集器会收集该对象。
因此,有finalize()的对象比没有finalize()的对象寿命长,垃圾收集器需要做更多的工作来收集它们。使用大量带finalize()的对象会增加内存压力,并有可能增加系统的内存需求。

为什么你需要对某些对象禁用finalize()呢?

让我们来看看Heinz指出的情况。java.awt.Graphics的实例(实际上是它的子类)保留了一个指向该对象所使用的本地资源的指针。dispose()方法释放了这些本地资源。它还有一个finalize(),在程序没有调用dispose()的情况下,调用dispose()作为一个 "安全网"。请注意,当一个Graphics对象变得不可触及时,它会被保留下来,以便被最终处理,即使程序已经调用了dispose()。

SunGraphics2D子类是一个 "轻量级 "对象,从来没有任何相关的本地资源。如果它继承了Graphics的终结器,那么为了运行终结器,实例就需要保留更长的时间,而终结器会调用dispose(),这样就会一无所获。为了防止这种情况,SunGraphics2D提供了一个空的finalize()方法。一个空的方法没有可见的副作用;因此,JVM为了运行一个空的finalize()方法而延长一个对象的生命周期是毫无意义的。相反,JVM会在确定这些对象无法到达时立即进行垃圾回收,跳过最终确定的步骤。

让我们看看这个动作。通过在一个对象的finalize()中加入打印语句,可以很容易地知道该对象何时被终结。但是,我们怎样才能知道一个具有空的finalize()的对象是否真的被终结了,或者它是否立即被垃圾回收了呢?这一点相当简单,使用JDK 18中新增加的JFR事件就可以做到。


下面是一个有小类层次结构的程序。A类有一个终结者;B继承了它;C用一个空终结者覆盖;D继承了空终结者;E用一个非空终结者覆盖。(我让它们成为嵌套在顶层类EmptyFinalizer内的静态类,所以它们都在一个文件中,但除此之外,这并不影响最终结果。请看完整的程序)。

static class A {
        protected void finalize() {
            System.out.println(this + " was finalized");
        }
    }

    static class B extends A {
    }

    static class C extends B {
        protected void finalize() { }
    }

    static class D extends C {
    }

    static class E extends D {
        protected void finalize() {
            System.out.println(this +
" was finalized");
        }
    }

主程序创建了一堆实例,但并没有保留对它们的引用。它调用System.gc()几次,然后睡觉,让垃圾收集器运行。输出结果类似于以下内容。

$ java EmptyFinalizer
EmptyFinalizer$E@cd4e940 was finalized
EmptyFinalizer$B@8eb6c02 was finalized
EmptyFinalizer$A@4de9e37b was finalized
EmptyFinalizer$E@57db5523 was finalized
EmptyFinalizer$B@7cee2871 was finalized
EmptyFinalizer$A@2f36c092 was finalized
EmptyFinalizer$E@2dc61c34 was finalized
EmptyFinalizer$B@203936e2 was finalized
EmptyFinalizer$A@2d193f34 was finalized
EmptyFinalizer$E@34324855 was finalized
EmptyFinalizer$B@2988c55b was finalized
EmptyFinalizer$A@40ef68ae was finalized
EmptyFinalizer$E@246b0f18 was finalized
EmptyFinalizer$B@23d8b20 was finalized
EmptyFinalizer$A@6df02421 was finalized

我们可以看到,A、B和E的实例被最终确定,但C和D没有被确定。好吧,我们真的无法判断,不是吗?他们的空终结器可能已经被调用了。从JDK 18开始,我们可以使用JFR来确定这些对象是否被最终化了。首先,在运行期间启用JFR。

$ java -XX:StartFlightRecording:filename=recording.jfr EmptyFinalizer
[0.365s][info][jfr,startup] Started recording 1. No limit specified, using maxsize=250MB as default.
[0.365s][info][jfr,startup] 
[0.365s][info][jfr,startup] Use jcmd 56793 JFR.dump name=1 to copy recording data to file.
EmptyFinalizer$A@cd4e940 was finalized
EmptyFinalizer$E@8eb6c02 was finalized
EmptyFinalizer$B@4de9e37b was finalized
EmptyFinalizer$A@57db5523 was finalized
EmptyFinalizer$E@7cee2871 was finalized
EmptyFinalizer$B@2f36c092 was finalized
EmptyFinalizer$A@2dc61c34 was finalized
EmptyFinalizer$E@203936e2 was finalized
EmptyFinalizer$B@2d193f34 was finalized
EmptyFinalizer$E@34324855 was finalized
EmptyFinalizer$B@2988c55b was finalized
EmptyFinalizer$A@40ef68ae was finalized
EmptyFinalizer$E@246b0f18 was finalized
EmptyFinalizer$B@23d8b20 was finalized
EmptyFinalizer$A@6df02421 was finalized

现在我们有了一个带有一堆事件的文件recording.jfr。接下来,我们用下面的命令以可读的形式打印这个文件。

$ jfr print --events FinalizerStatistics recording.jfr
jdk.FinalizerStatistics {
  startTime = 16:43:37.379 (2022-04-27)
  finalizableClass = EmptyFinalizer$A (classLoader = app)
  codeSource = "file:///private/tmp/"
  objects = 0
  totalFinalizersRun = 5
}

jdk.FinalizerStatistics {
  startTime = 16:43:37.379 (2022-04-27)
  finalizableClass = EmptyFinalizer$B (classLoader = app)
  codeSource =
"file:///private/tmp/"
  objects = 0
  totalFinalizersRun = 5
}

jdk.FinalizerStatistics {
  startTime = 16:43:37.379 (2022-04-27)
  finalizableClass = jdk.jfr.internal.RepositoryChunk (classLoader = bootstrap)
  codeSource = N/A
  objects = 1
  totalFinalizersRun = 0
}

jdk.FinalizerStatistics {
  startTime = 16:43:37.379 (2022-04-27)
  finalizableClass = EmptyFinalizer$E (classLoader = app)
  codeSource =
"file:///private/tmp/"
  objects = 0
  totalFinalizersRun = 5
}

我们可以很容易地看到,类A、B和E各有5个实例被最终处理,堆上剩下0个实例。C和D类没有被列出,所以没有对它们进行最终处理。另外,看起来JFR的内部类RepositoryChunk使用了一个终结器,有一个活的实例,没有被终结。(我们必须让JFR团队将这个类转换为使用Cleaner来代替!)

JEP 421已经废弃了finalize() 。最终,它将被禁用并从JDK中删除。如果你的系统使用finalize() --或者,也许更关键的是,如果你不知道你的系统是否使用finalize() --使用JFR来帮助找出答案。有关 JFR 的更多信息,请参见 JDK Flight Recorder 文档。