Java 18 Deprecates Finalization: Use Cleaner Instead
Introduction
Since Java 1.0, a class could override
OUTPUT of the above code
Here the finalizer happened to run, because the program explicitly requested a collection and waited. In a real application there is no such guarantee. Java 18 added the JVM option
OUTPUT of the above command
The finalizer never runs. Running your application and its tests with this option is the simplest way to find out whether anything depends on finalization. Java 18 also added a JFR event,
The replacement, part 1: try-with-resources
The right way to release a resource is explicitly and promptly, when the program is done with it. Make the class implement
OUTPUT of the above code
The replacement, part 2: Cleaner as a safety net
Sometimes an object owns a resource the garbage collector cannot see - native memory, or a handle from a C library - and you want a safety net in case a caller forgets to call
OUTPUT of the above code
Explanation of the code
Migrating away from finalize()
Object.finalize(), and the garbage collector would call it at some point before reclaiming the object - in theory, a last chance to release resources such as file handles or native memory. In practice, finalization has been a source of resource leaks, performance problems and security bugs for decades. finalize() was deprecated in Java 9, and Java 18Click to read tutorial: Overview of New Features in Java 18 (JEP 421) deprecated finalization for removal, and added an option to turn it off. This tutorial explains why finalization is flawed, how to test whether an application depends on it, and how to replace it with try-with-resources and the java.lang.ref.Cleaner API, with runnable examples.
What is wrong with finalization
- Unpredictable timing: a finalizer runs only after the garbage collector notices the object is unreachable, which may be much later - or never, if memory is plentiful. A program can run out of file handles long before the heap fills up.
- Performance cost: objects with finalizers are slower to allocate and must survive at least one extra garbage collection cycle, and finalizers run on a single background thread which can fall behind.
- Resurrection and security: a finalizer can store
thissomewhere, bringing a "dead" object back to life - which has been used in attacks on partially constructed objects. - No ordering and no error reporting: finalizers run in no particular order, on an unspecified thread, and exceptions thrown in them are silently ignored.
finalize(). The program creates one, drops the reference without closing it, and asks for a garbage collection -
A resource released by a finalizer
//FinalizerDemo.java
public class FinalizerDemo {
static class LegacyResource {
private final String name;
LegacyResource(String name) { this.name = name; System.out.println("Opened " + name); }
@Override
@SuppressWarnings({"deprecation", "removal"})
protected void finalize() {
System.out.println("finalize() released " + name);
}
}
public static void main(String[] args) throws InterruptedException {
new LegacyResource("file-1"); // never closed, reference dropped at once
System.gc(); // a request, not a guarantee
Thread.sleep(500); // give the finalizer thread time to run
System.out.println("Main finished");
}
}
Opened file-1 finalize() released file-1 Main finished
--finalization=disabled, which turns finalization off completely - the way it will behave once finalization is removed -
java --finalization=disabled FinalizerDemo
Opened file-1 Main finished
jdk.FinalizerStatistics, which reports which classes have finalizers and how often they run, and javac -Xlint:deprecation warns about classes which override finalize().
AutoCloseable, release the resource in close(), and use it in a try-with-resources statement, which calls close() even if an exception is thrown -
Releasing a resource with try-with-resources
//TryWithResourcesDemo.java
public class TryWithResourcesDemo {
static class Resource implements AutoCloseable {
private final String name;
Resource(String name) { this.name = name; System.out.println("Opened " + name); }
void use() { System.out.println("Using " + name); }
@Override public void close() { System.out.println("Closed " + name); }
}
public static void main(String[] args) {
try (Resource r = new Resource("file-1")) {
r.use();
} // close() runs here - immediately and predictably
System.out.println("Main finished");
}
}
Opened file-1 Using file-1 Closed file-1 Main finished
close(). For this, use java.lang.ref.Cleaner (Java 9). You register the object together with a cleaning action, a Runnable. The action runs once, either when close() calls clean() explicitly, or after the object becomes unreachable - whichever happens first -
Combining close() with a Cleaner safety net
//CleanerDemo.java
import java.lang.ref.Cleaner;
public class CleanerDemo {
private static final Cleaner CLEANER = Cleaner.create(); // one Cleaner shared by the class
static class NativeBuffer implements AutoCloseable {
// The state needed for cleanup. It must NOT refer back to the NativeBuffer,
// otherwise the buffer could never become unreachable.
private static class State implements Runnable {
private final String name;
State(String name) { this.name = name; }
@Override public void run() { System.out.println("Released native memory of " + name); }
}
private final Cleaner.Cleanable cleanable;
NativeBuffer(String name) {
System.out.println("Allocated " + name);
this.cleanable = CLEANER.register(this, new State(name));
}
@Override public void close() { cleanable.clean(); } // runs the action at most once
}
public static void main(String[] args) throws InterruptedException {
try (NativeBuffer b = new NativeBuffer("buffer-1")) {
System.out.println("Using buffer-1");
} // released promptly by close()
new NativeBuffer("buffer-2"); // forgotten: never closed
System.gc();
Thread.sleep(500);
System.out.println("Main finished");
}
}
Allocated buffer-1 Using buffer-1 Released native memory of buffer-1 Allocated buffer-2 Released native memory of buffer-2 Main finished
buffer-1is released byclose()at the end of thetryblock. Callingclean()also unregisters it, so the action never runs a second time.buffer-2was never closed. After it became unreachable and the garbage collector ran, theCleaner's background thread ran its action - the safety net. As with finalization, this timing is not guaranteed, which is whyclose()remains the primary mechanism.- Unlike
finalize(), the cleaning action cannot access or resurrect the object, because it only holds the separateState. A common mistake is to use a lambda or inner class which capturesthis- then the object stays reachable forever and is never cleaned.
- Run your application and tests with
--finalization=disabled, and use thejdk.FinalizerStatisticsJFR event or-Xlint:deprecationto find classes with finalizers - including in libraries, which may need upgrading. - Make resource-owning classes
AutoCloseable, and use them with try-with-resources. - Where a safety net is really needed, use a
Cleaneras shown above, keeping the cleanup state separate from the object. - Remove the
finalize()methods.
--finalization=disabled option. We then replaced it with try-with-resources for prompt release, and a Cleaner as a safety net for resources a caller forgets to close, and went through the steps to migrate. Next, read the overview of new features in Java 19Click to read tutorial: Overview of New Features in Java 19.Continue Reading Java 18 to 21 Tutorials
Overview of New Features in Java 18Click to read tutorial: Overview of New Features in Java 18 Java 18 UTF-8 by Default: What Changes and WhyClick to read tutorial: Java 18 UTF-8 by Default: What Changes and Why Java 18 Simple Web Server (jwebserver) TutorialClick to read tutorial: Java 18 Simple Web Server (jwebserver) Tutorial Java 18 Code Snippets in Javadoc with @snippetClick to read tutorial: Java 18 Code Snippets in Javadoc with @snippet Overview of New Features in Java 19Click to read tutorial: Overview of New Features in Java 19 Overview of New Features in Java 20Click to read tutorial: Overview of New Features in Java 20 Overview of New Features in Java 21Click to read tutorial: Overview of New Features in Java 21 Java 21 Virtual Threads Tutorial with ExamplesClick to read tutorial: Java 21 Virtual Threads Tutorial with Examples Java 21 Record Patterns with ExamplesClick to read tutorial: Java 21 Record Patterns with Examples
Overview of New Features in Java 18Click to read tutorial: Overview of New Features in Java 18 Java 18 UTF-8 by Default: What Changes and WhyClick to read tutorial: Java 18 UTF-8 by Default: What Changes and Why Java 18 Simple Web Server (jwebserver) TutorialClick to read tutorial: Java 18 Simple Web Server (jwebserver) Tutorial Java 18 Code Snippets in Javadoc with @snippetClick to read tutorial: Java 18 Code Snippets in Javadoc with @snippet Overview of New Features in Java 19Click to read tutorial: Overview of New Features in Java 19 Overview of New Features in Java 20Click to read tutorial: Overview of New Features in Java 20 Overview of New Features in Java 21Click to read tutorial: Overview of New Features in Java 21 Java 21 Virtual Threads Tutorial with ExamplesClick to read tutorial: Java 21 Virtual Threads Tutorial with Examples Java 21 Record Patterns with ExamplesClick to read tutorial: Java 21 Record Patterns with Examples