skip to content

Why was finalize() deprecated, and what should you use instead for resource cleanup?

level: seniorimportance: should knowfreq 48%

answer

  1. finalize = pre-GC hook, no timing/run guarantee
  2. Deprecated since Java 9
  3. Problems: slow (2 GC cycles), resurrection, swallowed exceptions
  4. Use try-with-resources + AutoCloseable (deterministic)
  5. Cleaner = safety net via phantom refs, can't resurrect

basics

~20 s

finalize() ran before garbage collection with no timing guarantee, was slow, and could even resurrect objects, so it was unreliable for cleanup. It's deprecated since Java 9. Use try-with-resources (AutoCloseable) for deterministic cleanup, and Cleaner as a safety net.

solid answer

~50 s

finalize() was Object's pre-GC hook: the garbage collector would call it before reclaiming an object. It was deprecated in Java 9 (and later scheduled for removal) because it's fundamentally unreliable: there's no guarantee it ever runs, when it runs, or on which thread; it can delay reclamation and hurt GC throughput; uncaught exceptions in it are swallowed; and a finalizer can 'resurrect' the object by storing a reference, defeating collection and complicating object lifecycle. The modern approach is deterministic cleanup via try-with-resources and the AutoCloseable interface (close() is called exactly when the block exits). For native or off-heap resources that need a last-ditch safety net if close() is forgotten, use java.lang.ref.Cleaner (built on phantom references and a ReferenceQueue), which keeps the cleanup action separate from the object so it can't resurrect it. The rule: own resources with try-with-resources; back them up with Cleaner, never finalize.

go deeper

for a junior

Knows finalize() is old/deprecated and that try-with-resources closes resources for you.

for a middle

Can list finalize's problems (no timing, slow) and use try-with-resources with AutoCloseable.

for a senior

Explains resurrection, double-GC cost, swallowed exceptions, and uses Cleaner correctly as a non-referencing safety net.

for a principal

Designs resource-ownership conventions across a codebase, reasons about native-memory lifecycle, Cleaner vs reachabilityFence pitfalls, and migration away from finalizers.

## What `finalize()` was `protected void finalize()` on `Object` was a hook the **garbage collector** would invoke on an object **before reclaiming its memory**, intended as a last chance to release resources (file handles, native memory). You overrode it to clean up. ## Why it was deprecated (Java 9) `finalize()` is broken in several deep ways: - **No timing guarantee**: there is *no promise it ever runs*, and no bound on *when* it runs after the object becomes unreachable. It depends entirely on when (or whether) the GC gets to it. So you cannot use it to free scarce resources promptly — file handles could be exhausted long before finalizers run. - **Performance penalty**: finalizable objects require *two* GC cycles to reclaim (one to discover unreachability and queue the finalizer, another to actually collect). This significantly slows GC and increases memory footprint. - **Unpredictable thread**: finalizers run on a JVM finalizer thread you don't control; an exception thrown in `finalize()` is **swallowed** and leaves the object half-cleaned. - **Object resurrection**: inside `finalize()` you can store `this` into a static field, making the object reachable again — 'resurrecting' it. This defeats collection and means `finalize()` may not be called again (it runs at most once per object). It massively complicates reasoning about lifecycle. - **Security**: finalizer attacks let a malformed subclass run code on a partially constructed object. ## The modern replacements ### 1. try-with-resources + `AutoCloseable` (the primary tool) For **deterministic** cleanup, implement `AutoCloseable` (or `Closeable`) and use try-with-resources. `close()` runs **exactly** when the block exits, in order, even on exception: ```java try (var in = new FileInputStream(path)) { // use in } // in.close() called here, deterministically ``` This is precise, prompt, and on the calling thread — everything `finalize()` is not. ### 2. `java.lang.ref.Cleaner` (the safety net) For a **backup** in case a caller forgets `close()`, or to release **native/off-heap** resources, use `Cleaner`. It registers a cleanup `Runnable` that runs after the object becomes phantom-reachable, via a `ReferenceQueue`: ```java public class Resource implements AutoCloseable { private static final Cleaner CLEANER = Cleaner.create(); private final Cleaner.Cleanable cleanable; private static class State implements Runnable { // must NOT reference the outer Resource public void run() { /* free native handle */ } } public Resource() { this.cleanable = CLEANER.register(this, new State()); } @Override public void close() { cleanable.clean(); } } ``` Crucially, the cleanup state is a **separate object that does not reference the owner**, so it can't resurrect it (the flaw finalize had). Cleaner still gives no strong timing guarantee — it's a *safety net*, not a substitute for `close()`. ## The takeaway Manage resources deterministically with **try-with-resources / AutoCloseable**; add a **Cleaner** as a defensive backstop for native resources. **Never** rely on `finalize()` — it's deprecated, unreliable, and being removed.

  • Why must a Cleaner's cleanup action not reference the object it cleans up?
    If the cleanup Runnable holds a reference to the owning object, that object stays strongly reachable and can never become phantom-reachable, so the Cleaner never runs — recreating finalize's resurrection problem. The state must be an independent object holding only the raw resource.
  • Does try-with-resources guarantee close() is called even if the body throws?
    Yes. The compiler emits a finally-like block; close() is invoked when the try block exits for any reason, including exceptions (with suppressed-exception handling), and resources are closed in reverse order of declaration.

saying these in an interview costs you the question

  • Treating finalize() as reliable resource cleanup
  • Assuming finalize() always runs (it may never)
  • Not knowing about try-with-resources / AutoCloseable
  • Letting the Cleaner's state object reference the owner (re-creates resurrection risk)
  • Believing Cleaner gives deterministic timing

context