What is the java.lang.ref.Cleaner API, and why did it replace Object.finalize()?
answer
- Cleaner.create(); register(obj, action) -> Cleanable
- Built on PhantomReference + ReferenceQueue + daemon thread
- Fixes finalize: no resurrection, no 2-cycle, no swallowed exceptions
- State must NOT capture the registered object (static nested class)
- Backstop only — prefer AutoCloseable / try-with-resources
basics
~20 sCleaner (Java 9+) is the modern, safe way to run cleanup when an object becomes unreachable. You register an object plus a cleanup action; Cleaner runs it later on a background thread. It replaced finalize() because finalizers were unpredictable, slow, and could resurrect objects or fail silently.
solid answer
~40 sjava.lang.ref.Cleaner, added in Java 9, lets you register an object with a cleanup Runnable; when the object becomes phantom-reachable, Cleaner runs that action on a dedicated background thread. Internally it builds on PhantomReference + ReferenceQueue, hiding the boilerplate. It replaced Object.finalize() (deprecated, removed in later JDKs) because finalizers were deeply flawed: no ordering or timing guarantees, finalizers could resurrect the dying object, an exception in a finalizer was swallowed, they ran on an unspecified thread, and finalizable objects took two GC cycles to reclaim, hurting performance and risking OOM. The critical rule with Cleaner is that the cleanup action must NOT capture a reference to the registered object, or it stays reachable and never cleans up. Cleaner is a backstop for native/off-heap resources; try-with-resources / AutoCloseable remains the primary, deterministic mechanism.
code
java · 27 linesimport java.lang.ref.Cleaner;
public class NativeBuffer implements AutoCloseable {
private static final Cleaner CLEANER = Cleaner.create();
// Must NOT reference NativeBuffer, or cleanup never runs.
private static final class State implements Runnable {
private long addr;
State(long addr) { this.addr = addr; }
@Override public void run() {
if (addr != 0) { freeNative(addr); addr = 0; }
}
}
private final State state;
private final Cleaner.Cleanable cleanable;
public NativeBuffer(long size) {
this.state = new State(allocNative(size));
this.cleanable = CLEANER.register(this, state);
}
@Override public void close() { cleanable.clean(); } // prompt + idempotent
private static native long allocNative(long size);
private static native void freeNative(long addr);
}go deeper
Knows Cleaner is the modern replacement for finalize() and runs cleanup when an object is no longer used.
Can register an object+action with Cleaner and explain it replaced finalize() because finalizers were unreliable.
Lists finalize()'s specific flaws (resurrection, two cycles, swallowed exceptions, timing), shows the static-state pattern, and the don't-capture rule.
Weighs Cleaner vs AutoCloseable vs Foreign Function & Memory API for native resources, custom thread factories, and reasons about non-deterministic cleanup in resource-management design.
## The problem Cleaner solves: releasing non-memory resources The garbage collector reclaims **Java heap memory** automatically, but it knows nothing about **native or off-heap resources** an object may own — a C malloc'd buffer, an OS file descriptor, a socket, a GPU handle. If such an object is collected without first releasing that resource, you **leak** it. You need a hook that fires when the object dies. ## The old way: Object.finalize() Historically you overrode `protected void finalize()`. Before reclaiming a finalizable object, the GC would queue it for a **finalizer thread** to run `finalize()`. This was so broken it was deprecated and later removed: - **No timing/ordering guarantees** — `finalize()` might run seconds later, much later, or *never* (e.g. if the JVM exits). Order across objects is unspecified. - **Resurrection** — `finalize()` receives `this`, a live reference, so it could store the object back into a reachable field, undoing collection and re-running... but `finalize()` runs **at most once**, so resurrected objects then leak. - **Two GC cycles** — a finalizable object survives one collection (to run the finalizer) and is reclaimed only on the next, increasing memory pressure and OOM risk. - **Swallowed exceptions** — an exception thrown in `finalize()` is ignored, leaving cleanup half-done silently. - **Unspecified thread + security/perf issues** — the finalizer thread can become a bottleneck or stall. ## The new way: java.lang.ref.Cleaner (Java 9+) `Cleaner` packages phantom-reference cleanup into a safe API: ```java static final Cleaner cleaner = Cleaner.create(); class FileHandle implements AutoCloseable { // State MUST be a separate class that does NOT reference FileHandle private static final class State implements Runnable { private long nativeFd; State(long fd) { this.nativeFd = fd; } public void run() { closeNative(nativeFd); } // the cleanup action } private final State state; private final Cleaner.Cleanable cleanable; FileHandle(long fd) { this.state = new State(fd); this.cleanable = cleaner.register(this, state); // (object, action) } public void close() { cleanable.clean(); } // deterministic, explicit } ``` `cleaner.register(obj, action)` associates `obj` with `action` and returns a `Cleanable`. **When `obj` becomes phantom-reachable**, the Cleaner's background thread runs `action`. Calling `cleanable.clean()` runs it **immediately and at most once** (idempotent), so try-with-resources gives deterministic cleanup *and* the Cleaner is the safety net if the user forgets to close. Under the hood, `Cleaner` maintains a `ReferenceQueue`, wraps each registration in a `PhantomReference` subclass, and runs a daemon thread that drains the queue and invokes the actions — exactly the manual pattern, productized. ## The cardinal rule: don't capture the object The cleanup action (here `State`) **must not hold a reference, direct or via a lambda capture, to the registered object** (`FileHandle`). If it did, the object would remain reachable through the Cleaner's internal phantom reference, never become phantom-reachable, and the cleanup would **never run** — a guaranteed leak. That's why `State` is a `static` nested class holding only the raw handle, not the outer instance. ## How Cleaner improves on finalize() - **No resurrection** — built on phantom references; the cleanup action never sees the object. - **Explicit, isolated cleanup logic** — a `Runnable`, not a method on the dying object. - **You control the thread/pool** — `Cleaner.create()` (default daemon) or `Cleaner.create(ThreadFactory)`. - **Deterministic option** — `Cleanable.clean()` for prompt release, idempotent with the GC-triggered path. - **Single reclaim cycle** semantics, less GC overhead than finalization. ## Important caveat Cleaner is still a **backstop**, not a primary API. Cleanup timing is non-deterministic (it fires whenever the GC decides). For correctness use **`AutoCloseable` + try-with-resources** for prompt, deterministic release, and register a Cleaner only as the safety net for forgotten closes. ## Key takeaways - Cleaner (Java 9+) = finalizer-free cleanup built on PhantomReference + ReferenceQueue. - It fixed finalize()'s resurrection, two-cycle, swallowed-exception, no-timing flaws. - The cleanup state must never capture the registered object. - Prefer try-with-resources; use Cleaner as a last-resort backstop.
- Why must the Cleaner cleanup action be a static nested class rather than a lambda or inner class referencing the outer object?A lambda or non-static inner class implicitly captures the enclosing instance. That strong reference keeps the registered object reachable, so it never becomes phantom-reachable and the cleanup never runs — a leak. A static nested class holds only the raw resource handle, breaking that reference.
- If you have Cleaner, why still implement AutoCloseable and use try-with-resources?Cleaner timing is non-deterministic — it runs whenever the GC collects the object, which could be much later or under memory pressure. try-with-resources releases the resource promptly and deterministically at the end of the block. Cleaner is the safety net for when callers forget to close.
saying these in an interview costs you the question
- Saying Cleaner runs immediately/deterministically when an object goes out of scope — it fires whenever the GC runs.
- Letting the cleanup Runnable capture 'this' / the registered object (it then never cleans up).
- Treating Cleaner as a replacement for try-with-resources rather than a backstop.
- Claiming finalize() guaranteed cleanup before exit — it could run late or never.
- Forgetting that Cleanable.clean() is idempotent (safe to call from close() and still have the GC path).