Contents

// 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); } }

Cleaner.create() returns a Cleaner instance backed by a daemon thread that processes cleaning actions. Passing an object and a Runnable to register() returns a Cleaner.Cleanable handle; the Runnable will be executed when the registered object becomes phantom-reachable (i.e., the GC determines nothing else can reach it through strong, soft, or weak references). The single most important constraint is that the cleaning action must not hold a reference back to the object being cleaned — if it does, the object can never become phantom-reachable and will never be collected, creating a permanent memory leak. The safe approach is a static nested class or a separate standalone class that captures only the primitive handles or resource identifiers needed for cleanup, never this.

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 The cleaning action must not hold a reference to the object being cleaned. If the lambda or inner class captures this, the object is strongly reachable from the Cleaner and will never be GC'd — creating a memory leak. Always use a static nested class or capture only primitive/immutable values.

The recommended pattern: implement AutoCloseable for explicit resource management, and use Cleaner as a safety net for forgotten close() calls:

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

The canonical pattern for native resource management combines AutoCloseable with Cleaner in two distinct layers. A static inner class (here Deallocator) holds the native memory address and implements Runnable — it has no reference to the outer object and contains only what is needed to free the resource. The outer class registers this state object with the Cleaner at construction time. Calling close() explicitly invokes cleanable.clean(), which runs the action immediately and deregisters it so the Cleaner thread will not run it again. If the caller forgets to close the object, the Cleaner catches the omission once the object is GC'd — making it a genuine safety net rather than a primary resource-release mechanism. The volatile closed flag prevents use-after-close bugs.

// 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(); } }
// 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(); } } The JDK itself uses Cleaner for DirectByteBuffer deallocation and FileDescriptor cleanup. It is the correct, supported way to manage native resources in modern Java — replacing both finalize() and manual PhantomReference patterns.