How to handle Java errors and cleanup without finalize

Java’s finalize method will be deprecated in Java 18 and removed entirely in a future release. Let’s look at the alternatives.

How to handle Java errors and cleanup without finalize

After several years of rumblings, Java is preparing to deprecate the finalize method in JDK 18. This is covered by JDK Enhancement Proposal 421, which will mark finalize as deprecated and allow for it to be turned off for testing. It will remain default enabled. It will be removed entirely in a future release. On this occasion, let’s take a look at what the end of finalize means and how we should handle errors and resource cleanup now.

What is finalize?

Before we understand why finalize is going away and what to use instead, let’s understand what finalize is or was.

The basic idea is to allow you to define a method on your objects that will execute when the object is ready for garbage collection. Technically, an object is ready for garbage collection when it becomes phantom reachable, meaning no strong or weak references are left in the JVM. 

At that moment, the idea is the JVM will execute the object.finalize() method, and application-specific code will then clean up any resources, such as I/O streams or handles to datastores.

The root Object class in Java has a finalize() method, along with other methods like equals() and hashCode(). This is to enable every single object in every Java program ever written to participate in this straightforward mechanism for avoiding resource leakages. 

Notice that this also addresses the cases where an exception is thrown and other cleanup code might be missed: the object will still be marked for garbage collection and its finalize method will eventually be called. Problem solved, right? Just override the finalize() method on your resource-consuming objects.

The problems with finalize

That’s the idea, but the reality is something else entirely. There are a number of shortcomings with finalize that prevent the realization of cleanup utopia. (This situation is similar to serialize(), another method that looked good on paper but became problematic in practice.)

Among the problems with finalize:

  • Finalize can run in unexpected ways. Sometimes the GC will determine your object has no live references to it before you think it will. Have a look at this rather scary Stack Overflow answer.
  • Finalize might never run, or will run after a long delay. On the other end of the spectrum, your finalize method might never run at all. As the JEP 421 RFC states, “GC typically operates only when necessary to satisfy memory allocation requests.” So you are at the mercy of the GC whim.
  • Finalize can resurrect otherwise dead classes. Sometimes, an object will fire an exception that makes it eligible for GC. However, the finalize() method gets its chance to run first, and that method could do anything including re-establishing live references to the object. This is a potential leak source and security hazard.
  • Finalize is difficult to implement correctly. Just straight-up writing a solid finalize method that is functional and error free is not as easy as it seems. In particular, there are no guarantees about the threading implications of finalize.  Finalizers can run on any thread, introducing error conditions that are very hard to debug. Forgetting to call finalize() can lead to hard-to-discover problems.
  • Performance. Given the unreliability of finalize in doing its stated purpose, the overhead to the JVM in supporting it is not merited.
  • Finalize makes for more fragile large-scale applications. The bottom line, as born out by research, is that large-scale software that uses finalize is more likely to be fragile, and to encounter hard-to-reproduce error conditions that arise under heavy loads.

Life after finalize

What are the proper ways to handle errors and cleanup now? We’ll look at three alternatives here: try-catch-finally blocks, try-with-resource statements, and cleaners. Each has its pluses and minuses.

Try-catch-finally blocks

The old fashioned way of handling resource release is via try-catch blocks. This is workable in many situations, but it suffers from being error-prone and verbose. For example, to fully capture nested error conditions (that is, when closing the resource also raises an exception), you need something like Listing 1. 

It might seem like overkill, but in a long-running and heavily used system, these kinds of conditions can spawn resource leaks that will kill an app. Therefore, alas, the verbosity has to be repeated across the codebase. These things are notorious for breaking up codeflow.

Listing 1. Handling resource closure with try-catch-finally

FileOutputStream outStream = null;
try {
  outStream = new FileOutputStream("output.file");
  ObjectOutputStream stream = new ObjectOutputStream(outStream);
  stream.write //…
} catch(FileNotFoundException ffe) {
  throw new RuntimeException("Could not open file for writing", ffee);
} catch(IOException ioe) {
  System.err.println("Error writing to file");
} finally {
  if (outStream != null) {
    try {
    } catch (Exception e) {
      System.err.println(“Failed to close stream”, e);

All you want to do in Listing 1 is open a stream, write some bytes to it, and make sure it gets closed, regardless of what exceptions are thrown. To do this, you have to wrap the calls in a try block, and if any checked exceptions are raised, deal with them (either by raising a wrapped runtime exception or by printing the exception to the log). 

You then need to add a finally block that double-checks the stream. This is to ensure that an exception didn’t prevent the closure. But you can’t just close the stream; you have to wrap it in yet another try block to make sure the closing doesn’t error out itself.

That’s a lot of work and disruption for a simple and common need.

Try-with-resource statements

Introduced in Java 7, a try-with-resource statement allows you to specify one or more resource objects as part of the try declaration. These resources are guaranteed to be closed when the try block completes.

Specifically, any class that implements java.lang.AutoCloseable can be supplied to try-with-resource. That covers almost every commonly used resource you’ll find in the Java ecosystem.

Let’s rewrite Listing 1 to make use of a try-with-resource statement, as seen in Listing 2.

Listing 2. Handling resource closure with try-with-resource

try (FileOutputStream outStream = new ObjectOutputStream(outStream)) {
  ObjectOutputStream stream = new ObjectOutputStream(outStream);
  stream.write //…
} catch(FileNotFoundException ffe) {
  throw new RuntimeException("Could not open file for writing", ffee);
} catch(IOException ioe) {
  System.err.println("Error writing to file");

You can see there are a number of benefits here that result in a smaller code footprint, but the greatest nicety is that once you hand off the stream (or whatever you are using) to the VM by declaring it inside the try block parenthesis, you don’t have to worry about it anymore. It will be closed for you. No resource leaks.

We’ve eliminated the need for a finally block or any calls to finalize. That addresses the main problem for most use cases (although the verbosity of checked error handling remains). 

There are some situations where a more elaborate and robust solution is required, when things are too complex to be handled in a single block like this. For those situations, the Java developer needs something more potent. For those situations, you need a cleaner.


The Cleaner class was introduced in Java 9. Cleaners allow you to define cleanup actions for groups of references. Cleaners produce a Cleanable implementation, which interface descends from Runnable. Each Cleanable runs in a dedicated thread that ignores exceptions. 

The idea here is to decouple the cleanup routine from the code that uses the objects requiring cleaning. Let’s make this more concrete with the example Oracle provides in the documentation, shown in Listing 3.

Listing 3. Simple cleaner example

public class CleaningExample implements AutoCloseable {
  // A cleaner, preferably one shared within a library
  private static final Cleaner cleaner = <cleaner>;
  static class State implements Runnable {
    State(...) {
      // initialize State needed for cleaning action
    public void run() {
      // cleanup action accessing State, executed at most once
  private final State;
  private final Cleaner.Cleanable cleanable
  public CleaningExample() {
    this.state = new State(...);
    this.cleanable = cleaner.register(this, state);
  public void close() {

To begin with, and perhaps most importantly, you can explicitly invoke the close() method to clean up your references. This is distinct from finalize(), which is fully dependent on the (indeterminate) call from the garbage collector.

If the close() call is not made explicitly, the system will execute it for you when the object passed as the first argument of cleaner.register() becomes phantom reachable. However, the system will not call close() if it has already been explicitly executed by you, the developer.

(Notice that the code example in Listing 3 produces an AutoCloseable object. That means it can be passed into the argument of a try-with-resource statement.)

Now for a caveat: Don’t create references to the cleaned up objects in the run method of your cleaner, because this will potentially create a zombie object (i.e., reestablish the object as live). This is unlikely to happen in the example format given, but becomes more likely if you implement this as a lambda (which has access to its enclosing scope).

Next, consider the comment, “A cleaner, preferably one shared within a library.” Why is that? It’s because each cleaner is going to spawn a thread, so sharing cleaners will result in lower overhead to the running program.

Finally (pun intended), notice that the object that is monitored is decoupled from the code (in the example, the State) which performs the cleanup work.

For a deeper dive on cleaners, see this article. It provides some good insight, in particular regarding what use cases merit their use (in this case, for disposing of expensive native resources).

Goodbye, finalize

Java keeps evolving. That is good news for those of us who love and use it. The deprecation of finalize() and the addition of new approaches are all good signs of this commitment to the future.

Copyright © 2022 IDG Communications, Inc.