Cleaners and finalizers are both unreliable, so what legitimate role does a Cleaner still play in resource management, and how is it wired up?
answer
- Cleaner = safety net + native peers only
- Still GC-scheduled: no timing guarantee
- Cleaning action must NOT capture the host object (static class)
- Pair with AutoCloseable; close() calls cleanable.clean()
- Safer than finalize: no swallowed exceptions, no attack
basics
~20 sA Cleaner is a best-effort backup. The real cleanup happens when a caller calls close() (via AutoCloseable/try-with-resources); the cleaner only runs later, if the caller forgot, to free a native resource that would otherwise leak — better late than never. It's a safety net, not the primary mechanism.
solid answer
~50 sEven though a Cleaner runs on the GC's schedule with no timing guarantee — the same defect that makes finalizers unreliable — it has two legitimate uses. First, as a safety net: if a caller forgets to call close(), the cleaner can still release a native resource late, which beats leaking it forever. Second, for cleaning up native peers — off-heap memory the GC can't reclaim on its own. The crucial wiring rule is that the cleanup state must be a static class (often a Runnable) that does NOT reference the host object, or the host can never become unreachable and the cleaner never runs. You register the object and its state with a shared Cleaner; the class also implements AutoCloseable so the deterministic path stays primary, and close() invokes the same cleaning action (idempotently) so it's safe whether close() or the GC triggers it. Cleaners are still strictly better than finalizers: no swallowed exceptions, no finalizer attack, and they can run on a controlled thread.
go deeper
Knows a Cleaner is a deprecated-finalizer replacement and that close()/try-with-resources is the real mechanism; details optional.
Can state the two legitimate uses (safety net, native peers) and that cleaners are still GC-scheduled.
Explains the no-capture wiring rule, pairing with AutoCloseable, idempotent clean(), and why cleaners beat finalizers despite the shared timing flaw.
Judges when a native-resource backstop is worth its cost, designs the close/cleaner contract for a library, and reasons about cleaner-thread behavior and JVM-exit semantics.
## Recap of the problem `finalize()` is deprecated and dangerous; the deterministic answer to resource cleanup is `AutoCloseable` + try-with-resources. But what if a caller *forgets* to close? For a pure-heap object that's fine (the GC handles memory), but for an object holding a **native resource** — off-heap memory, an OS handle obtained through native code — forgetting `close()` leaks that resource permanently. A `Cleaner` is a *best-effort backstop* for exactly this gap. ## What a Cleaner is `java.lang.ref.Cleaner` (Java 9+) lets you register an object together with a **cleaning action** (a `Runnable`). After the object becomes unreachable, the cleaner *eventually* runs the action on a dedicated cleaner thread. It is built on **phantom references** internally, runs cleanup on a controlled thread, and does **not** swallow exceptions the way finalizers do — so it fixes the security and exception problems of `finalize()`. What it does **not** fix is timing: like finalizers, there's no guarantee *when* — or, on abrupt JVM exit, *whether* — the action runs. ## The two legitimate uses 1. **Safety net.** Your class's primary contract is "call `close()`." The cleaner is a fallback that releases a native resource if the client neglected to close — "better late than never." Several JDK classes do this. 2. **Native peers.** A *native peer* is a non-Java object (e.g. C-allocated memory) that a Java object delegates to. The GC knows nothing about it, so when the Java object is collected, the native peer would leak. A cleaner can free the peer. (If the native resource is performance-critical or scarce, you still need explicit `close()` and use the cleaner only as backup.) ## The critical wiring rule: don't capture the host The cleaning action **must not refer to the object it is meant to clean.** If it did, that reference would keep the object reachable forever, so it would never become eligible for cleaning and the cleaner would never run — a self-defeating leak. The idiom is a **static nested class** holding only the resource state: ```java public class Room implements AutoCloseable { private static final Cleaner cleaner = Cleaner.create(); // Static so it doesn't capture the enclosing Room instance. private static final class State implements Runnable { int numJunkPiles; // the native/off-heap state to release State(int n) { numJunkPiles = n; } @Override public void run() { System.out.println("Cleaning room"); // release resource here numJunkPiles = 0; } } private final State state; private final Cleaner.Cleanable cleanable; public Room(int numJunkPiles) { state = new State(numJunkPiles); cleanable = cleaner.register(this, state); // register host + action } @Override public void close() { cleanable.clean(); } // deterministic path } ``` Note that `State` is `static` and references no `Room`. `close()` calls `cleanable.clean()`, which runs the action *immediately and at most once*; if the client uses try-with-resources, cleanup is deterministic. If the client forgets, the cleaner *may* run the same action later. Because `clean()` is idempotent, both paths are safe. ## Why even bother, given the timing caveat? Because for *native* resources, a late release is strictly better than a permanent leak, and a cleaner is far safer than a finalizer (controlled thread, no exception swallowing, no finalizer-attack vector). The cost is real, though — registering with a cleaner and the extra GC interaction aren't free — so use it only when there's a native resource worth backstopping. For ordinary Java objects, plain `AutoCloseable` with no cleaner is the right, cheaper choice. ## Summary Cleaner = best-effort safety net or native-peer cleanup, *never* the primary mechanism. Keep `AutoCloseable`/try-with-resources as the deterministic path, make the cleaning action a static type that doesn't capture the host, and make `close()` and the cleaner share one idempotent action.
- Why must the Cleaner's cleaning-action object not hold a reference to the object being cleaned?Because that reference would keep the host object strongly reachable, so it could never become eligible for garbage collection — and the cleaner only runs after the host is unreachable. The action would therefore never fire. That's why the idiom uses a static nested state class that captures no enclosing instance.
- How do close() and the cleaner cooperate without double-freeing?Both call the same Cleaner.Cleanable.clean(), which runs the registered action at most once. close() triggers it deterministically; if close() was never called, the cleaner may trigger it later. Because clean() is idempotent, whichever fires first wins and the second is a no-op.
saying these in an interview costs you the question
- Using a Cleaner as the primary cleanup instead of close()
- Letting the cleaning action reference the host object (prevents collection → cleaner never runs)
- Claiming Cleaner gives a timing guarantee finalizers lack
- Thinking a Cleaner is guaranteed to run before JVM exit