Java Object finalize: Why It's Deprecated and What to Use
Learn why java object finalize is deprecated, how the JVM invokes it, and why Cleaner and try-with-resources are safer for resource cleanup.
The finalize() method on java.lang.Object is invoked by the garbage collector before an object is reclaimed. That sounds like a convenient place to release native resources, but in practice it leads to unpredictable behavior and memory leaks. This article explains the runtime contract of java object finalize, why it is deprecated, and what to use instead.
The finalize() Method and Its Runtime Contract
Every Java class inherits a no-op finalize() method from Object. You can override it to perform cleanup before the object is garbage collected. The JVM calls finalize() at most once per object, and only when the object becomes unreachable and the GC decides to run the finalization process.
public class ResourceHolder { private native void releaseNativeResource(); @Override protected void finalize() throws Throwable { try { releaseNativeResource(); } finally { super.finalize(); } } }
The method is declared protected, so it can be overridden by subclasses. It may throw Throwable, but the JVM ignores any exception thrown during finalization. This means a failure in finalize() will not propagate to the caller or the GC; it is silently swallowed.
When the JVM Actually Calls finalize()
The JVM does not guarantee when finalize() runs, or even that it runs before the program terminates. The GC may defer finalization for a long time, and objects with finalize() are placed in a finalization queue after they become unreachable. They are not reclaimed until the finalizer thread processes them. This can cause objects to survive multiple GC cycles, increasing memory pressure and latency.
Consider a loop that creates many short-lived objects with finalize() overridden. The JVM must track each object for finalization, which adds overhead to allocation and GC. If the finalizer thread is slow, the queue grows, and the JVM may throw OutOfMemoryError even though most objects are unreachable.
Why finalize() Is Unreliable for Resource Cleanup
Resource cleanup in finalize() is inherently racy. There is no way to know when the method will be called, so a file handle or socket may remain open long after the object becomes unreachable. This defeats the purpose of deterministic resource release.
Another problem is that finalize() can accidentally resurrect an object. If the method assigns this to a static field or another reachable reference, the object becomes reachable again and the GC will not collect it. Even if it becomes unreachable later, finalize() will not be called a second time. This behavior is confusing and rarely intentional.
The combination of unpredictable timing, ignored exceptions, and resurrection risk makes finalize() unsuitable for managing scarce resources like database connections, file descriptors, or native memory.
Deprecation and Removal in Recent Java Versions
The finalize() method has been deprecated since Java 9. The deprecation is marked as @Deprecated(since = "9") and the Javadoc explicitly warns against using it. The Java platform team plans to remove finalization in a future release. JEP 421, "Deprecate Finalization for Removal," outlines the rationale and the migration path.
Removing finalization will affect existing libraries that rely on it. The JDK itself has been migrating internal usages to Cleaner or explicit resource management. For new code, there is no reason to use finalize(); the alternatives are safer and more predictable.
Modern Alternatives: Cleaner and try-with-resources
Java provides two main mechanisms to replace finalization:
java.lang.ref.Cleaner: ACleanerholds a set ofRunnableactions that run after an object becomes phantom-reachable. It is more deterministic thanfinalize()because you control when the cleaner is created and when the action is registered.try-with-resources: For resources that implementAutoCloseable, this construct guarantees thatclose()is called immediately after the block exits, even if an exception is thrown.
The table below summarizes the key differences:
| Aspect | finalize() | Cleaner | try-with-resources |
|---|---|---|---|
| Invocation timing | Nondeterministic | After phantom-reachability | Immediate on block exit |
| Exception handling | Ignored | Handled by cleaner thread | Propagated to caller |
| Resource type | Any object | Any object | Must implement AutoCloseable |
| Suitable for | Legacy code | Native memory, global caches | File, socket, DB connection |
Replacing finalize() with Cleaner: A Practical Example
Suppose you have a class that wraps a native resource. Instead of overriding finalize(), you can use a Cleaner to release the resource when the wrapper becomes unreachable.
import java.lang.ref.Cleaner; public class NativeResource implements AutoCloseable { private static final Cleaner CLEANER = Cleaner.create(); private final Cleaner.Cleanable cleanable; private long nativeHandle; public NativeResource() { nativeHandle = allocateNativeResource(); cleanable = CLEANER.register(this, () -> { if (nativeHandle != 0) { releaseNativeResource(nativeHandle); nativeHandle = 0; } }); } @Override public void close() { cleanable.clean(); } private native long allocateNativeResource(); private native void releaseNativeResource(long handle); }
The Cleaner action runs after the object is phantom-reachable, but you can also call clean() explicitly. This gives you deterministic cleanup when you need it, while still providing a safety net if the caller forgets to call close().
For resources that are used within a single scope, prefer try-with-resources:
try (NativeResource resource = new NativeResource()) { // use resource } // close() is called automatically
This pattern is simpler and guarantees that cleanup happens before the block exits, making resource usage predictable and easy to audit.
Performance and Maintainability Considerations
Using finalize() imposes a hidden cost on every object that overrides it. The JVM must track these objects and run a separate finalizer thread, which adds GC overhead and can delay reclamation. In contrast, Cleaner uses phantom references and a dedicated cleaner thread, but it is still a fallback mechanism, not a primary cleanup strategy.
For maintainability, finalize() is a trap. It is easy to forget that exceptions are ignored, that resurrection is possible, and that the method may never run. Modern code should make resource ownership explicit. try-with-resources makes the lifetime of a resource visible in the code, while Cleaner is appropriate for resources that outlive the method that created them, such as native memory caches.
Compatibility Notes for Existing Code
If you maintain a library that currently uses finalize(), plan to migrate before finalization is removed. The migration path depends on the resource type:
- If the resource implements
AutoCloseable, expose aclose()method and encourage callers to usetry-with-resources. - If the resource is a native handle or a global cache, use
Cleanerto register a cleanup action. - If the resource is a thread or a lock, reconsider the design; finalization is rarely appropriate for such objects.
During the transition, you can keep finalize() as a fallback but mark it deprecated and log a warning when it is invoked. This helps identify code paths that still rely on finalization. Remember that finalize() is called on an object that is already unreachable, so any state it accesses must be safe to read at that point.
Migrating away from finalize() is not just about avoiding a deprecated API. It makes resource cleanup deterministic, reduces GC pressure, and eliminates a class of bugs that are difficult to reproduce and debug.