Java Cleaner API: Safe Post-GC Resource Cleanup
The Cleaner API, introduced in Java 9 (java.lang.ref.Cleaner), provides a safe and predictable alternative to the deprecated finalize() method. It uses PhantomReferences internally to run cleanup actions after an object becomes phantom-reachable, without the severe performance and safety problems of finalizers. Always prefer explicit close() via AutoCloseable, and use Cleaner only as a safety net.
Contents
Why Cleaner Instead of finalize()
- finalize() is deprecated (Java 9) and removed in Java 18+. It has serious problems: unpredictable timing, can prevent GC, can resurrect objects, and runs on a single finalizer thread that blocks on errors.
- Cleaner advantages: runs on a dedicated thread that doesn't block other finalization; cleaning action runs in its own thread with no reference to the object; can be triggered explicitly; supports multiple cleaners for different resource types.
- Primary use: safety net cleanup for native memory, file handles, or other OS resources when
is not called explicitly.close()
// finalize() — don't use this
public class OldResource {
private long nativeHandle;
@Override
@Deprecated
protected void finalize() throws Throwable {
// Problems: unpredictable, can block GC, can resurrect 'this'
freeNative(nativeHandle);
}
}
Creating a Cleaner and Registering Objects
Cleaner.create()CleanerRunnableregister()Cleaner.CleanableRunnablethis
import java.lang.ref.Cleaner;
// Create a shared Cleaner (one per subsystem is typical)
// Runs cleanup actions on its own daemon thread
static final Cleaner CLEANER = Cleaner.create();
class Resource {
private final long handle;
private final Cleaner.Cleanable cleanable;
Resource(long handle) {
this.handle = handle;
// Register a cleanup action — it MUST NOT reference 'this'
// Use a static inner class or a lambda that captures only the handle
this.cleanable = CLEANER.register(this, new CleanupAction(handle));
}
// The cleaning action must be a static class (or standalone class)
// to avoid holding a reference back to the enclosing Resource object
private static class CleanupAction implements Runnable {
private final long handle;
CleanupAction(long handle) { this.handle = handle; }
@Override
public void run() {
System.out.println("Cleaning handle: " + handle);
// freeNative(handle);
}
}
public void use() { System.out.println("Using handle: " + handle); }
// Trigger cleanup immediately (optional — also runs automatically after GC)
public void cleanup() { cleanable.clean(); }
}
// Usage
Resource r = new Resource(42L);
r.use();
r.cleanup(); // explicit cleanup — idempotent (safe to call multiple times)
// If cleanup() was not called, CLEANER runs it after r becomes unreachable
thisCombining with AutoCloseable
The recommended pattern: implement AutoCloseableCleanerclose()
public class SafeResource implements AutoCloseable {
private static final Cleaner CLEANER = Cleaner.create();
// Holds state needed for cleanup — no reference to SafeResource
private static class State implements Runnable {
private boolean closed = false;
private final long nativeHandle;
State(long nativeHandle) { this.nativeHandle = nativeHandle; }
@Override
public void run() {
if (!closed) {
System.err.println("WARNING: Resource " + nativeHandle +
" was not explicitly closed!");
// freeNative(nativeHandle);
closed = true;
}
}
}
private final State state;
private final Cleaner.Cleanable cleanable;
public SafeResource(long nativeHandle) {
this.state = new State(nativeHandle);
this.cleanable = CLEANER.register(this, state);
}
@Override
public void close() {
state.closed = true; // mark as closed to suppress warning
cleanable.clean(); // run cleanup immediately and deregister
}
public void doWork() {
if (state.closed) throw new IllegalStateException("Resource is closed");
System.out.println("Working with: " + state.nativeHandle);
}
}
// Correct usage — no leak warning
try (SafeResource r = new SafeResource(99L)) {
r.doWork();
} // close() called automatically
// Forgot close() — Cleaner will print the warning after GC
SafeResource forgotten = new SafeResource(100L);
forgotten.doWork();
// forgotten becomes unreachable → Cleaner prints warning
Native Resource Example
The canonical pattern for native resource management combines AutoCloseableCleanerDeallocatorRunnableCleanerclose()cleanable.clean()closed
// Wrapping a native library handle (C library loaded via JNI/Panama)
public class NativeBuffer implements AutoCloseable {
private static final Cleaner CLEANER = Cleaner.create();
private static final class Deallocator implements Runnable {
private long address;
Deallocator(long address) { this.address = address; }
@Override
public void run() {
if (address != 0) {
freeNativeBuffer(address); // native call
address = 0;
}
}
}
private final long address;
private final int capacity;
private final Cleaner.Cleanable cleanable;
private volatile boolean closed = false;
public NativeBuffer(int capacity) {
this.capacity = capacity;
this.address = allocateNativeBuffer(capacity); // native call
this.cleanable = CLEANER.register(this, new Deallocator(address));
}
public void putByte(int offset, byte value) {
checkOpen();
putNativeByte(address + offset, value); // native call
}
public byte getByte(int offset) {
checkOpen();
return getNativeByte(address + offset);
}
private void checkOpen() {
if (closed) throw new IllegalStateException("Buffer is closed");
}
@Override
public void close() {
closed = true;
cleanable.clean();
}
}
Pitfalls and Best Practices
- Cleaner is a safety net, not a primary resource manager. Always implement
and use try-with-resources.AutoCloseable - Timing is not guaranteed. The GC may not run for a long time: don't rely on Cleaner for timely resource release (e.g., database connections, file locks).
- Never capture
in the cleaning action. Use a static nested class capturing only the necessary state.this - Cleaner.clean() is idempotent. Safe to call multiple times: only the first call runs the action.
- One Cleaner per subsystem is the recommended pattern: don't create a new
per object.Cleaner
// BAD — captures 'this', prevents GC
class Broken implements AutoCloseable {
private final Cleaner.Cleanable cleanable;
private long handle;
Broken(long handle) {
this.handle = handle;
// Lambda captures 'this' — the object will NEVER be GC'd!
cleanable = CLEANER.register(this, () -> freeNative(this.handle));
}
@Override public void close() { cleanable.clean(); }
}
// GOOD — static class captures only the handle value
class Fixed implements AutoCloseable {
private static final Cleaner CLEANER = Cleaner.create();
private final Cleaner.Cleanable cleanable;
private static class Cleanup implements Runnable {
private final long handle;
Cleanup(long h) { this.handle = h; }
@Override public void run() { freeNative(handle); }
}
Fixed(long handle) {
cleanable = CLEANER.register(this, new Cleanup(handle));
}
@Override public void close() { cleanable.clean(); }
}
CleanerDirectByteBufferFileDescriptorfinalize()PhantomReference