skip to content

What is java.lang.ref.Cleaner, why does it replace finalize(), and how do you use it correctly?

level: seniorimportance: should knowfreq 52%

answer

  1. Cleaner = phantom ref + queue + daemon thread; Java 9+
  2. Replaces finalize() (unpredictable, can resurrect, extra GC cycle)
  3. Cleanup Runnable must NOT capture the object (use static nested class)
  4. Backstop only — prefer AutoCloseable + try-with-resources
  5. Cleanable.clean() = explicit, idempotent, at-most-once

basics

~20 s

Cleaner runs a cleanup action after an object becomes unreachable, using phantom references and a background thread. It replaces finalize(), which was unreliable and is deprecated. The key rule: the cleanup action must not reference the object it cleans up.

solid answer

~50 s

Cleaner (Java 9+) is the modern replacement for finalize(). You register an object with a cleanup Runnable; when the object becomes phantom-reachable, the Cleaner's background thread runs that Runnable to release native or off-heap resources. It's built on PhantomReference + ReferenceQueue, so it can't resurrect the object and won't keep it alive — provided the cleanup action holds no reference back to the registered object. That's the critical pitfall: capture only the state needed to free the resource (e.g. a file descriptor or pointer), typically in a separate static class or a held Runnable, never the outer instance, or the object never becomes unreachable. Cleaner is a safety net, not the primary mechanism: the right approach is still try-with-resources / AutoCloseable for deterministic cleanup, with Cleaner as a backstop for callers who forget to close. The returned Cleanable also lets you trigger cleanup explicitly and idempotently.

code

java · 24 lines
java
import java.lang.ref.Cleaner;

public final class Handle implements AutoCloseable {
    private static final Cleaner CLEANER = Cleaner.create();

    // static => no hidden reference to the Handle instance
    private static final class State implements Runnable {
        private final long fd;
        State(long fd) { this.fd = fd; }
        @Override public void run() { closeFd(fd); } // release OS resource
    }

    private final Cleaner.Cleanable cleanable;

    public Handle(long fd) {
        this.cleanable = CLEANER.register(this, new State(fd));
    }

    @Override public void close() { cleanable.clean(); } // idempotent, deterministic

    private static native void closeFd(long fd);
}
// Preferred usage: try (Handle h = new Handle(open())) { ... }
// Cleaner only fires if the caller forgets to close().

go deeper

for a junior

Knows finalize() is discouraged and that Cleaner runs cleanup when an object is no longer used; can use try-with-resources.

for a middle

Explains Cleaner is built on phantom references, replaces finalize(), and that you register an object with a cleanup action plus a Cleanable to trigger it.

for a senior

Articulates why finalize() is bad, the no-capture pitfall (static State class), idempotent clean(), and that Cleaner is a backstop to AutoCloseable.

for a principal

Reasons about cleaner-thread sizing/sharing, ordering and timeliness guarantees (none), interaction with native memory lifecycles, and designing resource wrappers that are leak-proof under both explicit close and GC paths.

## The problem Cleaner solves Some Java objects wrap a **non-memory resource** the GC can't free: a native memory buffer, an OS file descriptor, a socket handle. When the Java wrapper object is collected, that underlying resource also needs to be released. Historically you'd override **`finalize()`**, a method the GC called before reclaiming an object. But `finalize()` is badly flawed and is now **deprecated for removal**: - **Unpredictable timing** — it runs whenever (or never) the finalizer thread gets to it; resources can stay open arbitrarily long. - **Resurrection** — a finalizer can store `this` into a static field, making the object reachable again, which corrupts the GC's reasoning and means finalizers can run on already-"dead" objects. - **Extra GC cycle** — finalizable objects survive one collection (to run the finalizer) then must be collected again, hurting throughput. - **Exception swallowing & unspecified thread** — errors are dropped silently. ## What `Cleaner` is `java.lang.ref.Cleaner` (introduced in **Java 9**) is a managed facility built on **`PhantomReference` + `ReferenceQueue` + a daemon worker thread**. You: 1. Create (or share) a `Cleaner` instance: `Cleaner cleaner = Cleaner.create();` 2. **Register** an object plus a **cleanup `Runnable`**: `Cleanable c = cleaner.register(obj, runnable);` 3. When `obj` becomes **phantom-reachable** (no longer reachable any stronger way), the cleaner's thread runs `runnable` to release the resource. 4. The returned **`Cleanable`** lets you call `clean()` yourself to run the action **early and at-most-once** (it's idempotent). Because it sits on phantom references, the action **cannot access or resurrect** the object, and registering does **not** delay collection — assuming you avoid the one big pitfall. ## The critical pitfall: don't capture the object The cleanup `Runnable` **must not hold a reference to the object being cleaned** (directly, or via a captured lambda / inner class). If it does, the Runnable — which the Cleaner keeps alive until it runs — keeps the object **strongly reachable forever**, so it never becomes phantom-reachable and the cleanup **never runs**. The canonical fix is a **static nested class** (or a lambda capturing only primitives/handles) that holds *only* the resource state needed to free it: ```java public class NativeBuffer implements AutoCloseable { private static final Cleaner CLEANER = Cleaner.create(); // Holds ONLY the native pointer — NOT the NativeBuffer instance. private static final class State implements Runnable { private final long ptr; State(long ptr) { this.ptr = ptr; } public void run() { freeNative(ptr); } // release native memory } 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(); } // deterministic, idempotent } ``` Note `State` is **static** so it doesn't implicitly capture the enclosing `NativeBuffer` (a non-static inner class would). ## How to use it correctly (rules) 1. **Prefer deterministic cleanup**: implement `AutoCloseable` and use **try-with-resources**. Cleaner is a **backstop** for callers who forget `close()`, not the primary path. 2. **Make the action self-contained**: capture only the resource handles, never the wrapper instance. 3. **Keep the action fast and non-blocking** — it runs on a shared cleaner thread. 4. **Make cleanup idempotent** — `Cleanable.clean()` runs at most once, so calling `close()` then GC won't double-free. 5. **Share a `Cleaner`** across many objects rather than creating one per object (each `Cleaner` owns a thread). ## Where it fits in this topic Cleaner is the concrete, recommended application of **phantom references** from the `java.lang.ref` ladder, and it operates entirely on **heap reachability** (the object must become unreachable from all GC roots before the action fires). It is the bridge between the GC's reachability machinery and your resource-management code.

  • Why must the Cleaner cleanup action be defined so it doesn't reference the registered object?
    The Cleaner keeps the action (and anything it captures) strongly reachable until it runs. If the action references the object, the object stays strongly reachable forever, never becomes phantom-reachable, and the cleanup never fires — a leak. So you put the freeing logic in a static class holding only the raw resource handle.
  • Should Cleaner replace try-with-resources?
    No. try-with-resources / AutoCloseable gives deterministic, prompt release and should be the primary mechanism. Cleaner is a safety net for callers who forget to close, releasing the resource eventually when the object is collected.

saying these in an interview costs you the question

  • Capturing the registered object (or using a non-static inner class) in the cleanup action — prevents collection, cleanup never runs
  • Treating Cleaner as deterministic / primary cleanup instead of a backstop to AutoCloseable
  • Creating a new Cleaner per object (each spawns a thread)
  • Assuming the cleanup action can touch the object's fields (it can't — object is already unreachable)

context