finalize and Resource Cleanup

Java's original finalize() mechanism for pre-collection cleanup is deprecated and unreliable — modern Java uses AutoCloseable, try-with-resources, and cleaners for deterministic resource management instead.

Object.finalize() was Java's original answer to 'how do I run cleanup code before an object's memory is reclaimed'. In practice, it has always been a poor fit for that job, and Java's evolution reflects that: finalize() is deprecated since Java 9 and on a removal trajectory, replaced by explicit, deterministic mechanisms — chiefly AutoCloseable with try-with-resources, and for truly last-resort native cleanup, the java.lang.ref.Cleaner API.

Historical Finalization

Object declares a protected finalize() method that a subclass could override; historically, the garbage collector would call an object's finalize() at most once, at some point after the object became unreachable and before its memory was actually reclaimed (if the collector ever ran it at all before JVM shutdown).

class Legacy {
    @Override
    @SuppressWarnings("deprecation")
    protected void finalize() throws Throwable {
        System.out.println("finalize called - historical pattern, avoid in new code");
        super.finalize();
    }
}

Why It Is Unreliable

finalize() has several fundamental problems: there is no guarantee it runs promptly after an object becomes unreachable, or even that it runs at all before the JVM exits; it runs on an unspecified, JVM-managed finalizer thread, not the thread that made the object unreachable, so ordering relative to other work is unpredictable; an overridden finalize() can 'resurrect' the object by storing a new reference to this somewhere reachable, defeating collection entirely; and a slow or exception-throwing finalize() can degrade the whole finalization subsystem for the process.

This pattern is exactly why finalize() is considered dangerous: it lets an object interfere with its own collection.

class Resurrector {
    static Resurrector zombie;
    @Override
    protected void finalize() {
        zombie = this; // resurrection - object becomes reachable again, defeating GC
    }
}

Deprecation and Removal Trajectory

Object.finalize() was marked @Deprecated(since="9") in Java 9, with the Javadoc explicitly recommending AutoCloseable/try-with-resources or Cleaner as replacements. It remains present for backward compatibility with legacy code but should not be used, or relied upon, in new code — it is a strong candidate for eventual removal from the platform.

AutoCloseable

AutoCloseable (Java 7+) is a simple functional interface with one method, close(), that a class implements to expose deterministic cleanup. Unlike finalize(), nothing about AutoCloseable is automatic on its own — close() only runs when something explicitly calls it, most commonly via try-with-resources.

class ManagedFile implements AutoCloseable {
    ManagedFile(String path) { System.out.println("opened " + path); }
    void write(String data) { System.out.println("writing: " + data); }
    @Override public void close() { System.out.println("closed"); }
}

try-with-resources

try-with-resources (Java 7+) is syntax that guarantees close() is called on every declared resource, in reverse declaration order, when the try block exits — whether normally or via an exception. This is the deterministic replacement for cleanup logic that used to be attempted, unreliably, via finalize().

Multiple resources can be declared in one try-with-resources statement, separated by semicolons; they are closed in the reverse of their declaration order, so a later resource that might depend on an earlier one is still closed first.

try (ManagedFile f = new ManagedFile("a.txt")) {
    f.write("hello");
    throw new RuntimeException("boom");
} catch (RuntimeException e) {
    System.out.println("caught: " + e.getMessage());
}
// Output order: opened a.txt / writing: hello / closed / caught: boom
// close() runs BEFORE the catch block, guaranteed, even though an exception was thrown.

Cleaners: A Specialized Safety Net

java.lang.ref.Cleaner (Java 9+) is the modern, safer alternative for the narrow case finalize() used to serve: a genuine safety-net cleanup that should run if a caller forgets to call close() explicitly. A Cleaner registers a cleaning action tied to an object's reachability, but — critically — the cleaning action must not reference the object being cleaned (avoiding finalize()'s resurrection problem), and it runs on a dedicated cleaner thread once the object becomes phantom-reachable.

The Cleaner is a safety net for callers who forget close(); the primary, expected cleanup path is still an explicit close() call, ideally via try-with-resources.

A Cleaner is not a substitute for calling close() - it exists as a backstop, typically for native-memory-holding libraries, and still has no strict timing guarantee, only somewhat more predictable and safer semantics than finalize().

import java.lang.ref.Cleaner;

class NativeResource implements AutoCloseable {
    private static final Cleaner CLEANER = Cleaner.create();
    private final Cleaner.Cleanable cleanable;
    private final State state;

    private static class State implements Runnable {
        // must NOT hold a reference back to NativeResource itself
        @Override public void run() { System.out.println("native handle released (safety net)"); }
    }

    NativeResource() {
        this.state = new State();
        this.cleanable = CLEANER.register(this, state);
    }

    @Override public void close() {
        cleanable.clean(); // primary, deterministic path - explicit close()
    }
}

Deterministic Resource Management

The overarching lesson across all of Java's evolution here: cleanup tied to garbage collection timing is inherently non-deterministic and should never be the primary mechanism for releasing scarce or externally-visible resources (file handles, sockets, locks, database connections). Deterministic management — explicit close() calls, ideally via try-with-resources — should always be the primary path; GC-adjacent mechanisms like Cleaner exist only as a safety net for bugs, not as the intended design.

Practical Usage

When designing a class that wraps any resource needing cleanup (files, sockets, connections, locks, native memory), implement AutoCloseable and document that callers must use try-with-resources (or call close() in a finally block in pre-Java-7 style code, though try-with-resources is strongly preferred). Reserve Cleaner for library authors adding a safety net around native resources; application code almost never needs to use Cleaner directly.

try (var in = new FileInputStream("in.txt");
     var out = new FileOutputStream("out.txt")) {
    out.write(in.readAllBytes());
} // both closed automatically, out closed first (reverse declaration order), then in

Edge Cases

A close() method that itself throws an exception during try-with-resources cleanup, when the try block also threw, results in the try block's original exception being the one propagated, with the close() exception attached as a suppressed exception (retrievable via Throwable.getSuppressed()) rather than replacing it — this avoids silently losing the original, usually more important, failure.

class Flaky implements AutoCloseable {
    @Override public void close() { throw new IllegalStateException("close failed"); }
}
try (Flaky f = new Flaky()) {
    throw new RuntimeException("primary failure");
} catch (RuntimeException e) {
    System.out.println(e.getMessage());              // primary failure
    System.out.println(e.getSuppressed().length);    // 1
    System.out.println(e.getSuppressed()[0].getMessage()); // close failed
}

Mental Model

Think of finalize() as an unreliable, informal 'if you happen to notice this object lying around unclaimed, maybe clean it up sometime' note left for a janitor who might be on vacation. try-with-resources is a formal, guaranteed checkout process: you must explicitly return the resource (close()) when you're done, and the system enforces that you do, no matter how you leave (normally or via exception). Cleaner is an emergency backup notice, filed just in case the checkout process was skipped by mistake — not a substitute for actually checking the resource back in.

Quick Reference

finalize()       : deprecated (Java 9+), unreliable timing, DO NOT USE in new code
AutoCloseable     : interface with close(); implement for any resource-holding class
try-with-resources: guarantees close() runs, even on exception, reverse declaration order
Cleaner (Java 9+) : safety-net cleanup tied to phantom reachability, for library authors only

try (Resource r = acquire()) {
    use(r);
} // r.close() guaranteed here

Key Takeaways

finalize() is deprecated since Java 9 and unreliable: it may run late, on an unpredictable thread, or never before JVM shutdown, and can even resurrect the object.

AutoCloseable plus try-with-resources is the modern, deterministic replacement - close() is guaranteed to run when the try block exits, normally or via exception.

Multiple resources in one try-with-resources statement are closed in reverse declaration order.

Cleaner (Java 9+) provides a safer safety net than finalize() for library authors, but is not a substitute for explicit close() calls.

If both the try block and close() throw, the try block's exception propagates with the close() exception attached as suppressed, not lost.

class Resource { @Override protected void finalize() { release(); } // unreliable cleanup path class Resource implements AutoCloseable { @Override public void close() { release(); } } // used with try-with-resources finalize() has no timing guarantee and may never run before JVM shutdown. AutoCloseable with try-with-resources gives guaranteed, deterministic cleanup.

FileInputStream in = new FileInputStream("f.txt"); try { process(in); } finally { // forgot to call in.close() here } try (FileInputStream in = new FileInputStream("f.txt")) { process(in); } // close() called automatically, impossible to forget try-with-resources eliminates the risk of a forgotten close() call in a manual finally block by making cleanup automatic and guaranteed.

class State implements Runnable { NativeResource owner; // holds a reference back to the object being cleaned public void run() { owner.releaseNative(); } } class State implements Runnable { long nativeHandle; // holds only the raw data needed, NOT a reference to the owning object public void run() { releaseNativeHandle(nativeHandle); } }

A Cleaner's cleaning action must never reference the object being cleaned, or it prevents that object from ever becoming phantom-reachable - the same resurrection-style problem finalize() had.

Why is finalize() considered unreliable for resource cleanup?

It has no guaranteed timing (may run late or not at all before JVM shutdown), runs on an unpredictable finalizer thread, and an override can resurrect the object by re-establishing a reference to it, defeating garbage collection entirely.

What is the modern, recommended replacement for finalize()-based cleanup?

Implement AutoCloseable with a close() method, and have callers use try-with-resources, which guarantees close() runs deterministically when the try block exits, whether normally or via exception.

In what order are multiple resources closed in a single try-with-resources statement with several declared resources?

In the reverse of their declaration order - the last-declared resource is closed first.

What is Cleaner, and how does it avoid finalize()'s resurrection problem?

Cleaner (Java 9+) registers a cleaning action tied to an object's phantom reachability; critically, the cleaning action (typically a separate Runnable/state object) must not hold a reference back to the object being cleaned, so it cannot resurrect that object the way an overridden finalize() could.

If a try block throws an exception and the resource's close() also throws during cleanup, which exception is the one that propagates?

The try block's original exception propagates; the exception thrown by close() is attached to it as a suppressed exception (accessible via getSuppressed()), rather than replacing or hiding the original failure.

Should application code (not library authors) typically use Cleaner directly?

No. Cleaner is intended as a safety net for library authors managing resources like native memory; ordinary application code should rely on explicit AutoCloseable/try-with-resources as the primary, expected cleanup mechanism.

Why does Java's deprecation of finalize() reflect a broader design principle?

It reflects a shift toward deterministic, explicit resource management rather than tying cleanup to non-deterministic garbage collection timing - correctness-critical cleanup (closing files, releasing locks, returning pooled connections) should never depend on when, or whether, GC happens to run.

Write a class PooledConnection implementing AutoCloseable that prints 'acquired' on construction and 'released' in close(), used correctly in a try-with-resources block.

class PooledConnection implements AutoCloseable { PooledConnection() { System.out.println("acquired"); } void query() { System.out.println("querying"); } @Override public void close() { System.out.println("released"); } } try (PooledConnection conn = new PooledConnection()) { conn.query(); }

Prints: acquired / querying / released. close() is guaranteed to run at the end of the try block, deterministically, without relying on garbage collection or finalize().

What is the output order?

using / B closed / A closed. Resources declared in a try-with-resources statement are closed in the reverse of their declaration order, so B (declared second) closes before A (declared first).

Write a try-with-resources example where both the try block and the resource's close() throw exceptions, then show how to retrieve the suppressed exception in the catch block.

class Flaky implements AutoCloseable { public void close() { throw new IllegalStateException("close failed"); } } try (Flaky f = new Flaky()) { throw new RuntimeException("main failure"); } catch (RuntimeException e) { System.out.println("caught: " + e.getMessage()); for (Throwable suppressed : e.getSuppressed()) { System.out.println("suppressed: " + suppressed.getMessage()); } }

Output: caught: main failure / suppressed: close failed. The try block's exception is the one that propagates and is caught; close()'s exception is attached as a suppressed exception rather than replacing the original.

What problem does this finalize() override demonstrate, and how does Cleaner avoid it?

It demonstrates resurrection: by storing a reference to itself (this) in a static field during finalize(), the object becomes reachable again, defeating garbage collection. Cleaner avoids this by design - its registered cleaning action is a separate object that must not hold a reference back to the object being cleaned, so there is no way for the cleaning action to resurrect it.

class A implements AutoCloseable {
    public void close() { System.out.println("A closed"); }
}
class B implements AutoCloseable {
    public void close() { System.out.println("B closed"); }
}
try (A a = new A(); B b = new B()) {
    System.out.println("using");
}
class Zombie {
    static Zombie ref;
    @Override
    protected void finalize() {
        ref = this; // resurrects the object
    }
}