When I had heard that finalization is (finally!) marked for removal, I was really happy. Some time in the future it will be gone for good from the JDK, which means less code, higher reliability, tighter security and a tad more performance. Marking something for removal from JDK is an extremely rare event. After all, source compatibility is one of the strongest Java’s points - you can just grab some source code from version X and it’s a really big surprise if it doesn’t compile in verson X+N. What is so bad about the finalization that it had to be marked for removal?

A long and painful list of finalization issues

Soon after Java 1.0 appeared, it was evident there were a number of problems with finalization. This is one of the situations in which, no matter how hard we try, we can’t cover all the problems related to or caused by something. What follows is a definitely-not-final list of headaches caused by finalization in Java.

Unpredictable order

Any moderately complex application can have thousands of objects created and destroyed. Perhaps after some time finalizers will be called for some of them. But JVM gives no guarantee for the order of finalization.

Unpredictable timeliness/latency/threading

The amount of time that passes between an object becoming unreachable and its finalizer is run is unknown. Even worse, whether finalizer will run at all is unknown. Since we want finalizers to run so that OS resources can be properly closed and reused, this unpredictableness is definitely not what we want. As a consequence, resource exhaustion can happen and it can make not only our application unstable but all the other applications running concurrently on the same hardware. It is unspecified on which thread it will be run. It is even unspecified how many threads can run finalizers concurrently. More (unspecified) threads, more probability for deadlock.

Unconstrained behavior

A finalizer is just a normal Java code and it can do anything, and I literally mean anything. It can resurrect the very object being finalized, i.e. making the reference to that object reachable again. It can start 10000 threads. It can block its thread of execution. Have I already mentioned it can be run at arbitrary time, perhaps while your application is doing some very important time-sensitive processing?

Always enabled

There was just no way for a JVM or an application to turn off or at least to delay finalization. Luckily, JDK 18 gives us an option to disable it completely with --finalization=disabled.

Breaks subclass/superclass relationship

If a subclass has implemented finalization but it didn’t call super.finalize() then finalizer of a superclass won’t be called and its resources won’t be freed. Ooops!

JLS, JDK and JVM are more complex because of it

Finalization API is public, therefore all Java specifications and implementations (such as The Java® Language Specification and The Java® Virtual Machine Specification) are more complex because of it. Finalization is a part of an object lifecycle which is already complex enough.

It can be called too early

Under some (rare) circumstances, finalization can even be called too early, while an object isn’t fully constructed yet. This can lead to very interesting side-effects like intermittent exceptions thrown from a thread over which we don’t have any control of when and how is run.

Performance suffers

There are two-faced penalties for using finalizers. First one is that garbage collection is slower because it (maybe) will schedule and run finalizers. The other one is that finalizers run also on objects that are already properly cleaned up, for example close() method has already been called either manually or automatically at the end of try-with-resources block.

Libraries that use finalization

I’m lucky enough that I never had to write finalizer in production code. But even if you’re in the same situation, our applications always consists of code that someone else has written, whether it’s part of JDK or some library that we use. And the situation in the trenches is that a lot of popular libraries use finalization - Guava, Apache Commons, Apache POI, Netty, Aspose and probably many others. As the matter of fact, if we have a .jar file with library’s source code, we can check for the usage of finalization by using [jdeprscan](https://docs.oracle.com/en/java/javase/17/docs/specs/man/jdeprscan.html) tool. The command to check all JARs in a single directory is jdeprscan *.jar 2> /dev/null | rg finalize. I encourage you to run it in a directory that contains all your project dependencies and see which of them use finalization. It’s probable that newer versions of those dependencies will move away from finalization.

Compatibility with previous JDKs

Finalization is still enabled in JDK 18, although all of JDKs code has now marked finalize methods for removal. As mentioned before, if we start an app with --finalization=disabled, finalization will never run. It’s probable that some future JDK will make this the default behavior and possibly emit a warning if finalizers were used during application execution.

What to use instead?

If you (still) write finalizers you’re more qualified to provide this answer than me. But in a nutshell, our options are:

  • use try-with-resources and/or implement AutoCloseable interface on all classes whose objects may hold resources
  • use Cleaner API for very long-lived objects
  • add --finalization=disabled and monitor behavior of your app with regards to resource usage

Official JEP specification

If you want to dive deeply into specification of this feature, you may find it in JEP 421.

Dear fellow developer, thank you for reading this article about deprecation of finalization in JDK 18. Until next time, TheJavaGuy saluts you!