Java 18 Deprecates Finalization: Use Cleaner Instead

Introduction Since Java 1.0, a class could override 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 this somewhere, 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.
Finalization in action, and --finalization=disabled The class below "holds" a resource and releases it in 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");
  }
}
 OUTPUT of the above code
Opened file-1
finalize() released file-1
Main finished
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 --finalization=disabled, which turns finalization off completely - the way it will behave once finalization is removed -
java --finalization=disabled FinalizerDemo
 OUTPUT of the above command
Opened file-1
Main finished
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, jdk.FinalizerStatistics, which reports which classes have finalizers and how often they run, and javac -Xlint:deprecation warns about classes which override finalize().
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 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");
  }
}
 OUTPUT of the above code
Opened file-1
Using file-1
Closed file-1
Main finished
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 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");
  }
}
 OUTPUT of the above code
Allocated buffer-1
Using buffer-1
Released native memory of buffer-1
Allocated buffer-2
Released native memory of buffer-2
Main finished
Explanation of the code
  • buffer-1 is released by close() at the end of the try block. Calling clean() also unregisters it, so the action never runs a second time.
  • buffer-2 was never closed. After it became unreachable and the garbage collector ran, the Cleaner's background thread ran its action - the safety net. As with finalization, this timing is not guaranteed, which is why close() remains the primary mechanism.
  • Unlike finalize(), the cleaning action cannot access or resurrect the object, because it only holds the separate State. A common mistake is to use a lambda or inner class which captures this - then the object stays reachable forever and is never cleaned.
Migrating away from finalize()
  1. Run your application and tests with --finalization=disabled, and use the jdk.FinalizerStatistics JFR event or -Xlint:deprecation to find classes with finalizers - including in libraries, which may need upgrading.
  2. Make resource-owning classes AutoCloseable, and use them with try-with-resources.
  3. Where a safety net is really needed, use a Cleaner as shown above, keeping the cleanup state separate from the object.
  4. Remove the finalize() methods.
Summary In this tutorial we saw why finalization, deprecated for removal in Java 18, is unreliable, slow and unsafe. We watched a finalizer release a resource only because the program forced a garbage collection, and saw it never run with the new --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.