Explain the object resurrection problem with finalize(), and why an object's finalize() never runs a second time.
answer
- this escapes -> object reachable again
- Resurrection: stash this in static/collection
- finalize at-most-once -> never re-runs
- Second death leaks resource silently
- Cleaner action holds NO back-reference
basics
~20 sInside finalize(), the object can store 'this' somewhere reachable, making itself live again (resurrected). Because finalize() runs at most once per object, the JVM won't call it again later, so any future cleanup is silently skipped.
solid answer
~50 sWhen the GC decides an object is unreachable, it runs that object's finalize() on the finalizer thread. Inside finalize(), the code has access to 'this'. If it assigns 'this' to a static field, a live collection, or any reachable structure, the object becomes reachable again — it is 'resurrected' and not collected. The trap is the at-most-once rule from the JLS: the JVM flags that finalize() has already been called for this instance, so even after the object becomes unreachable a second time, finalize() will NOT run again. The resource the finalizer was supposed to release will leak on the second death. This makes finalize() fundamentally unsafe for cleanup that must happen reliably. It's one of several reasons finalization is deprecated; Cleaner avoids the issue because its cleanup action doesn't (and must not) hold a reference back to the registered object, so resurrection isn't expressible.
go deeper
Can state that finalize() runs at most once and that there's a notion of an object becoming reachable again, without needing the full mechanism.
Explains that finalize() can store 'this' to resurrect the object and that finalize() won't run a second time, so a later cleanup is skipped.
Connects resurrection + at-most-once to a concrete resource leak, contrasts with Cleaner's no-back-reference design, and cites it as a deprecation driver.
Reasons about why the platform cannot defend against finalizer resurrection or finalizer-attack patterns, and mandates resource-ownership designs (handle objects, Cleaner backstops) that make resurrection structurally impossible.
## Background you need first In **Java**, the **garbage collector (GC)** frees objects that are no longer **reachable** — meaning no chain of references from a live root (a stack variable, a static field, etc.) can reach them. `finalize()` was an `Object` method the GC would call *once*, just before reclaiming such an object, to let it clean up. Two facts combine to create the bug: 1. **finalize() runs arbitrary code with `this` available.** It is an instance method, so inside it the keyword `this` refers to the very object being finalized. 2. **The JLS guarantees finalize() runs *at most once* per object — ever.** Once the JVM has invoked it for an instance, it sets an internal flag and will never invoke it again for that same instance. ## What 'resurrection' is Because `this` is in scope, a `finalize()` implementation can make the object reachable again — by storing `this` into a `static` field, adding it to a live `List`, registering it with a still-live listener, etc. The GC was about to delete it; now it can't, because the object is reachable. The object has been *resurrected* (brought back to life). ```java class Zombie { static Zombie INSTANCE; @Override protected void finalize() { INSTANCE = this; // resurrects: 'this' is now reachable from a static field } } ``` After the first GC pass, `Zombie`'s `finalize()` runs and stashes `this` in `INSTANCE`. The object survives. ## Why the second death is fatal to cleanup Now suppose later we do `Zombie.INSTANCE = null;`. The object becomes unreachable a *second* time. You might expect `finalize()` to run again so it can clean up — but it will **not**. The at-most-once flag is already set. So this time the GC silently reclaims the object **without** running any finalizer. If the finalizer was the thing meant to release a native handle, that handle now leaks permanently, with no error and no log. This is why resurrection is not a clever trick but a **correctness hole**: it makes the *only* cleanup hook unreliable in exactly the situation where you'd want it. ## Why this is impossible to fully defend against You can't generally prevent a subclass's `finalize()` (or a malicious one) from resurrecting an object, and you can't ask the JVM to re-finalize. This unpredictability — combined with the performance cost, exception swallowing, and lack of timing guarantees — is a core reason finalization is **deprecated since Java 9** and slated for removal (**JEP 421**, Java 18). ## How Cleaner avoids it `java.lang.ref.Cleaner` (Java 9+) is the modern safety-net. You register a *cleanup action* — a `Runnable` — with the Cleaner, associated with the object. The critical design rule: **the cleanup action must not reference the object it's cleaning up**. If it did, it would keep the object permanently reachable (preventing collection entirely). Because the action holds only the *state needed to clean up* (e.g. a native pointer wrapped in a separate holder), there is no `this` to resurrect, and the structural possibility of resurrection simply doesn't exist. The Cleaner is built on `PhantomReference`, whose `get()` always returns `null`, so the referent can never be reached through it. ```java class NativeResource implements AutoCloseable { private static final Cleaner CLEANER = Cleaner.create(); // State holder: NO reference back to NativeResource private static final class Handle implements Runnable { private final long ptr; Handle(long ptr) { this.ptr = ptr; } public void run() { freeNative(ptr); } // can't resurrect anything } private final Cleaner.Cleanable cleanable; NativeResource(long ptr) { this.cleanable = CLEANER.register(this, new Handle(ptr)); } public void close() { cleanable.clean(); } // deterministic path private static native void freeNative(long ptr); } ``` **Bottom line:** resurrection + the at-most-once rule make `finalize()` unable to guarantee cleanup. Prefer deterministic `close()` via `AutoCloseable`, and use a `Cleaner` (whose action holds no back-reference) only as a backstop.
- Why must a Cleaner cleanup action not reference the object it cleans up?Because the Cleaner keeps the action reachable; if the action referenced the object, the object would stay reachable forever and never become eligible for cleanup. The action should capture only the standalone state (e.g. a native pointer) needed to release the resource.
saying these in an interview costs you the question
- Believing finalize() re-runs each time the object becomes unreachable
- Thinking you can safely 'undo' resurrection to get a second finalize() call
- Designing a Cleaner action that captures 'this' (defeats the purpose, prevents collection)
- Treating resurrection as a useful object-pooling technique