Now that JEP 421 (Deprecate Finalization for Removal) has been delivered in JDK 18, it seems like more people are talking about finalization and how to migrate to alternatives such as Cleaner
. I had an interesting Twitter conversation about this with Heinz Kabutz the other day:
The code from SunGraphics2D
that Heinz pointed out is this:
@SuppressWarnings("removal")
public void finalize() {
// DO NOT REMOVE THIS METHOD
}
Why did somebody bother to write an empty finalize()
method, and why is it so important that there is a comment warning not to remove it?
The answer is that an empty finalizer disables finalization for all instances of that class and for all instances of subclasses (unless overridden by a subclass). Depending on the usage of that class, this can be a significant optimization.
To understand this, let’s recap the Java object life cycle.
A Java object without a finalizer is created, is used for a while, and eventually becomes unreachable. Some time later, the garbage collector notices that the object is unreachable and garbage collects it.
An object with a finalizer is created, is used for a while, and eventually becomes unreachable. Some time later, the object’s finalize()
method is run. This is regular Java code, so the object is now actually reachable. Some additional time later, the object becomes unreachable again, and this time, the garbage collector collects the object. Thus, objects with finalizers live longer than objects without finalizers, and the garbage collector needs to do more work to garbage collect them. Using a lot of objects with finalizers increases memory pressure and potentially increases the memory requirements of the system.
Why would you need to disable finalization for some objects?
Let’s look at the case that Heinz pointed out. Instances of java.awt.Graphics
(actually, its subclasses) keep a pointer to native resources used by that object. The dispose()
method frees those native resources. It also has a finalizer that calls dispose()
as a “safety net” in case the program didn’t call dispose()
. Note that when a Graphics
object becomes unreachable, it’s kept around in order for it to be finalized, even if the program had already called dispose()
.
The SunGraphics2D
subclass is a “lightweight” object that never has any associated native resources. If it were to inherit the finalizer from Graphics
, instances would need to be kept around longer in order to run the finalizers, which would call dispose()
, which would do nothing. To prevent this, SunGraphics2D
provides an empty finalize()
method. An empty method has no visible side effects; therefore, it’s pointless for the JVM to extend the lifetime of an object in order to run an empty finalize()
method. Instead, the JVM garbage collects such objects as soon it can determine they are unreachable, skipping their finalization step.
Let’s see this in action. It’s pretty easy to tell when an object is finalized by putting a print statement into its finalizer. But how can we tell whether an object with an empty finalizer was actually finalized or whether it was garbage collected immediately? This is fairly simple to do, using a new JFR event added in JDK 18.
Here’s a program with a small class hierarchy. Class A has a finalizer; B inherits it; C overrides with an empty finalizer; D inherits the empty finalizer; and E overrides with a non-empty finalizer. (I’ve made them static classes nested inside a top-level class EmptyFinalizer
so they’re all in one file, but otherwise this doesn’t affect finalization. See the full program.)
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");
}
}
The main program creates a bunch of instances but doesn’t keep references to them. It calls System.gc()
a few times and sleeps to let the garbage collector run. The output is something like the following:
$ 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
We can see that instances of A
, B
, and E
were finalized, but C
and D
were not. Well, we can’t really tell, can we? Their empty finalizers might have been called. Starting in JDK 18, we can use JFR to determine whether these objects were finalized. First, enable JFR during the run:
$ 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
Now we have a file recording.jfr
with a bunch of events. Next, we print this file in a readable form with the following command:
$ 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
}
We can easily see that classes A
, B
, and E
each had five instances finalized, with zero instances remaining on the heap. Classes C
and D
aren’t listed, so no finalization was performed for them. Also, it looks like the JFR internal class RepositoryChunk
uses a finalizer, and there was one live instance, and none were finalized. (We’ll have to get the JFR team to convert this class to use Cleaner
instead!)
JEP 421 has deprecated finalization for removal. Eventually it will be disabled and removed from the JDK. If your system uses finalizers — or, perhaps more crucially, if you don’t know whether your system uses finalizers — use JFR to help find out. See the JDK Flight Recorder documentation for more information about JFR.
(Updated with suggestions from Kim Barrett. Thanks, Kim!)
[…] Why Write an Empty finalize() Method?https://stuartmarks.wordpress.com/2022/04/27/why-write-an-empty-finalize-method/ […]
pues… también depende de qué es lo que querramos que haga la aplicación, para determinar la necesidad de crear el método finalize
(I’m assuming that Google Translate did a reasonable job with this, as I don’t read Spanish.) Of course whether a finalize() method is appropriate or necessary depends on the application, or more precisely, what the objects are intended to do. A common use case for finalizers is to clean up native resources that are associated with an object. In the example, the `Graphics` object is associated with some kind of native graphics context object. However, its subclass `SunGraphics2D` has no corresponding native object. That’s why it’s appropriate for `SunGraphics2D` to disable finalization for itself (and its subclasses) by providing an empty finalizer.
[…] >> Why Write an Empty finalize() Method? [stuartmarks.wordpress.com] […]