Replacing Finalizers with Cleaners

Cleaners were introduced in JDK 9 as a mechanism to call cleanup functions for heap objects that have security sensitive content or encapsulate non-heap resources. JEP 421: Deprecate Finalization for Removal describes the rationale for replacing finalizers and alternatives for developers. As JEP 421 moves forward, the adoption of Cleaner as a convenient alternative to finalization is getting more interest from developers. These examples show several ways to use cleanup functions to replace finalizers.

Previously, the cleanup would be done in the finalize method of the class. Even though the object is un-reachable, when the finalize method is called it has access to all of fields of the object and can change them.

Cleanup functions, like finalizers, are run when an object is found to be unreachable from any class or thread. Unlike a finalizer, a cleanup function holds the state needed for cleanup separately from the object because we want the object to be reclaimed as soon as it is unreachable. The cleanup function must be able to work independently from the object. If there are any references to the object from the cleanup function it would still be reachable and could not be reclaimed. Any state needed for the cleanup must be encapsulated in the cleanup function.

I’ll use italics to refer to instances including object O, cleanup function F, Cleaner C, and Cleanable A to help keep track of what’s what.

An object O and corresponding cleanup function F is registered with a Cleaner. The registration is usually done in the constructor of object O. The Cleaner returns a Cleanable holding the reference to the object O and its cleanup function F. Calling Cleanable.clean() runs the cleanup function at most once. The Cleaner has a thread that waits for registered objects to become unreachable and runs the corresponding cleanup function F. A Cleaner and its thread is fairly heavyweight and should be shared within an application, package, or class where possible.

Due of the close relationship between the object O, the cleanup function F, and the Cleaner, there are several potential coding pitfalls to watch out for. A concrete example illustrates how to write a cleanup function to take advantage of the garbage collector as it reclaims objects.

The class SensitiveData holds data that should be erased when it is no longer in active use. The data is erased in a cleanup function (initially using a lambda) by clearing the internal char array. It is called either by the close method or by the Cleaner when its instances become unreachable. SensitiveData implements AutoCloseable to encourage its use within try-with-resources promoting the idea that cleanup should occur as early as possible.

In un-structured contexts, where try-with-resources is not suitable, the close method should be called directly. The Cleaner serves as a fallback when a SensitiveData object is no longer referenced and the cleanup has not been called by the close method.

In the example, we’ll show the cleanup function is implemented using a lambda or a class SensitiveCleanable. All the state needed for the cleanup is encapsulated in the cleanup function.

import java.lang.ref.Cleaner;
import java.util.Arrays;
import java.util.Optional;

public class SensitiveData implements AutoCloseable {
  // A cleaner
  private static final Cleaner cleaner = Cleaner.create();

  // The sensitive data
  private char[] sensitiveData;
    
  // The result of registering with the cleaner
  private final Cleaner.Cleanable cleanable;

  /**
   * Construct an object to hold sensitive data.
   */
  public SensitiveData(char[] sensitiveData) {
    final char[] chars = sensitiveData.clone();
    final Runnable F   // Pick one 
      = () -> Arrays.fill(chars, (char) 0);// A lambda      
      // = new SensitiveCleanable(chars);  // A record 
      // = clearChars(chars);              // A static lambda
    this.sensitiveData = chars;
    this.cleanable = cleaner.register(this, F);
  }

  /**
   * Return an Optional of a copy of the char array.
   */
  public Optional<char[]> sensitiveData() {
    return Optional.ofNullable(sensitiveData == null
          ? null : sensitiveData.clone());
  }

  /**
   * Close and cleanup the sensitive data storage.
   */
  public void close() {
    sensitiveData = null;   // Data not available after close
    cleanable.clean();
  }

  /*
   * Return a lambda to do the clearing.
   */
  private static Runnable clearChars(char[] chars) {
    return () -> Arrays.fill(chars, (char)0);
  }

  /*
   * Nested record class to perform the cleanup of an array.
   */
  private record SensitiveCleanable(char[] sensitiveData) 
                 implements Runnable {
    public void run() {
      Arrays.fill(sensitiveData, (char)0);
    }
  }
}

A brief example showing SensitiveData used with try-with-resources and the clearing of temporary arrays to minimize the time the sensitive data is visible in process memory.

public class Main {
  // Encapsulate a string for printing
  public static void main(String[] args) {
    for (String s : args) {
      char[] chars = s.toCharArray();
      try (var sd = new SensitiveData(chars)) {
        Arrays.fill(chars, (char) 0);
        print(sd);
      }
    }
  }

  // Print the sensitive data and clear
  private static void print(SensitiveData sd) {
    char[] chars = sd.sensitiveData().get();
    System.out.println(chars);
    Arrays.fill(chars, (char) 0);
  }
}


Coding the Cleanup Function

Let’s take a closer look as the pros and cons of various cleanup function coding choices. Each of these expose a FunctionalInterface method with no arguments that performs the cleanup using only values it holds itself.


A lambda cleanup function

class Foo {
  private final char[] data;
    
  Foo(char[] chars) {
    final char[] array = chars.clone();
    cleaner.register(this, 
          () -> Arrays.fill(array, (char)0));
    this.data = array;
  }
}

For simple cleanup, a lambda is the concise and can be coded in-line in a constructor. It is easy to code but it may be hard to spot mistakes and verify that it is working as expected. For example, if chars is a field, the lambda could refer to this.chars inadvertently capturing this. Unless there were tests written to check that it was cleared, you might not notice that the the object is not collected and the cleanup does not occur.

One way to ensure that this is not captured is to create the lambda in a static method. Its scope does not have this so it cannot accidentally be captured.

private static Runnable clearChars(char[] chars) {
  return () -> Arrays.fill(chars, (char)0);
}


A cleanup function as record, nested, or top level class

class Foo {
  // Record class to clear an array.
  private record Cleanup(char[] array)
          implements Runnable {
    public void run() {
      Arrays.fill(array, (char) 0);
    }
  }

  private char[] data;

  // Copy the array and register the cleanup.
  Foo(char[] chars) {
    final char[] array = chars.clone();
    cleaner.register(this, new Cleanup(array));
    this.data = array;
  }
}

A record class, nested static class, or top level class is the most robust way to write a successful cleanup function. It provides good visibility into the separation of the state that is independent of the object and provides a place to document the cleanup.

Do not be tempted to make the cleanup function an inner class, it implicitly refers to this and the cleanup will not be run.


Cleanup Using Mutable State

Though in most cases the record or nested class fields are immutable, there are use cases where the state needed for cleanup is changed during the normal use of the object O and therefore needs to be mutable. Holding the mutable state in an object accessible to both object O and the cleanup function F is a straight-forward way to handle this case. If multiple threads are modifying the state, they will need to have some kind of synchronization. When a Cleaner is shared among different uses, and synchronization is needed for the state, the cleanup function might block the Cleaner thread. As with any synchronization, check carefully that the synchronization does not result in deadlock or delay in the cleanup. For example, if the cleanup involved closing a network connection, it would be prudent to fork off a task independent of the Cleaner thread.


Wrap up

The most lightweight cleanup function can, with careful attention to detail, be a lambda. The lambda must not refer to this implicitly or explicitly. The lambda body cannot refer to fields in the constructor. This is more restrictive than the usual allowance to use final or effectively final values.

Converting to cleanup functions from finalizers may require a bit of refactoring to separate out the state. The refactoring can improve the structure and reliability of the class.

The complete example with the various options is available from SensitiveData.java.

~

Originaly posted here.