Guide to Finalization with Java Cleaners

After the legacy Java finalization has been deprecated (JEP-421) in Java 18, we are supposed to use any one of the two available approaches for resource cleanups:

In this tutorial, we will learn the basics of Cleaner API and its use.

1. Need for Cleaners

1.1. Problems with Finalizers

As discussed in detail in why finalize() should not be used, this method is invoked by the garbage collector when there are no strong or weak references to this object left in the JVM. 

At this moment, Java runtime will execute Object.finalize() method on such orphan objects and application-specific code will then clean up any resources, such as I/O streams or handles to datastores.

Though the implementation seems simple, it might never run or will run after a long delay because GC typically operates only when necessary to satisfy memory allocation requests. So we are dependent on GC to invoke it, which is highly unpredictable.

Another problem is that finalizers can run on any thread, introducing error conditions that are very hard to debug, just like any other concurrency issue. If such issues arise in production, they are hard to reproduce and debug.

1.2. How do Cleaners Help?

Cleaners, introduced in Java 9, allow us to define cleanup actions for groups of objects. Cleaners are implemented with Cleanable interface that descends from Runnable.

Each Cleanable represents an object and a cleaning action registered in a Cleaner. Each Cleanable runs in a dedicated thread. All exceptions thrown by the cleaning action are ignored

The most efficient use is to explicitly invoke the clean() method when the object is closed or no longer needed.

Note that the cleaning action must not refer to the object being registered. If so, the object will not become phantom reachable and the cleaning action will not be invoked automatically.

For this reason, do not use inner classes for implementing cleaning actions because an inner class implicitly holds the reference to the outer object, which prevents it from being garbage collected.

The behavior of cleaners during System.exit is implementation-specific. No guarantees are made relating to whether cleaning actions are invoked or not.

2. Implementation Guidelines

Oracle provides a sample implementation reference in the docs:

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() {
    cleanable.clean();
  }
}
  • Notice the close() method. We can call this method explicitly in the application code to trigger the resource cleanup process.
  • JVM will automatically invoke the close() method if the developer has not explicitly invoked it.

The CleaningExample class implements the AutoCloseable interface so we can use this class inside try-with-resources statement as well.

Each Cleaner spawns a thread, so creating only a single Cleaner for the whole application or library is recommended. Remember that it is expensive to create new threads.

3. How to Implement Cleaners

A Cleaner object can be created using its static method create as shown below. This method creates a Cleaner instance and starts a daemon thread which keeps on monitoring the objects eligible for garbage collection.

Cleaner cleaner = Cleaner.create();

Next, we need to register the object and cleanup action using the register() method. This method takes two arguments:

  • An object that the cleaner keeps monitoring for garbage collection.
  • java.lang.Runnable instance which represents the cleanup action to be taken.
cleaner.register(object, runnable);

Finally, we can either call the clean() method ourselves or wait for the GC to invoke it. The clean() method unregisters the runnable and invokes the cleaning action.

runnable.clean();

4. Demo

In this demo, we create a resource simulation with class Resource that is not doing anything in this example.

public class Resource {
  //Demo resource
}

Further, we discussed that there should be only one Cleaner instance per application because of thread creation overheads, so we are creating the cleaner in a utility class.

import java.lang.ref.Cleaner;

public class AppCleanerProvider {
  private static final Cleaner CLEANER = Cleaner.create();    
  
  public static Cleaner getCleaner() {
    return CLEANER;
  }
}

Now we need to write a class that will have access to the Resource as well as the Cleaner. We want when ClassAccessingResource is garbage collected, the cleanResourceAction() method should be invoked to free the Resource.

ClassAccessingResource implements the AutoClosable interface also to make it compatible with try-with-resources statements. This is optional. We can write the close() method and call it ourselves.

import java.lang.ref.Cleaner;

public class ClassAccessingResource implements AutoCloseable {
  
  private final Cleaner cleaner = AppCleanerProvider.getCleaner();
  private final Cleaner.Cleanable cleanable;

  //This resource needs to be cleaned after usage
  private final Resource resource;

  public ClassAccessingResource() {
    this.resource = new Resource();
    this.cleanable = cleaner.register(this, cleanResourceAction(resource));
  }
  
  public void businessOperation() {
    //Access the resource in methods
    System.out.println("Inside businessOperation()");
  }
  
  public void anotherBusinessOperation() {
    //Access the resource in methods
    System.out.println("Inside anotherBusinessOperation()");
  }

  @Override
  public void close() throws Exception {
    cleanable.clean();
  }

  private static Runnable cleanResourceAction(final Resource resource) {
    return () -> {
      // Perform cleanup actions
      // resource.release();
      System.out.println("Resource Cleaned Up !!");
    };
  }
}

To demonstrate the resource cleaning, I have created instances of ClassAccessingResource and invoked the cleaner in both ways: explicitly and implicitly.

public class CleanerExample {
  public static void main(final String[] args) throws Exception {
    
    //1 Implicit Cleanup
    try (final ClassAccessingResource clazzInstance 
           = new ClassAccessingResource()) {
      // Safely use the resource
      clazzInstance.businessOperation();
      clazzInstance.anotherBusinessOperation();
    }
    
    //2 Explicit Cleanup
    final ClassAccessingResource clazzInstance = new ClassAccessingResource();
    clazzInstance.businessOperation();
    clazzInstance.anotherBusinessOperation();
    clazzInstance.close();
  }
}

Notice the output. In both ways, the resource cleanup is triggered once.

Inside businessOperation()
Inside anotherBusinessOperation()
Resource Cleaned Up !!

Inside businessOperation()
Inside anotherBusinessOperation()
Resource Cleaned Up !!

5. Conclusion

Though Cleaners are not as straightforward as the finalize() method, they provide more control on the timing when JVM triggers the resource cleaning. They are not completely dependent on the garbage collection and can be triggered by the developers.

Moving forward, we must remove all finalize() methods from the application source code and start using Cleaners, as recommended by the Oracle.

Happy Learning !!

Sourcecode on Github

Was this post helpful?

Join 7000+ Awesome Developers

Get the latest updates from industry, awesome resources, blog updates and much more.

* We do not spam !!

Leave a Comment

HowToDoInJava

A blog about Java and related technologies, the best practices, algorithms, and interview questions.