New in Java 18: Deprecation of Finalization Mechanism
Leave finalization in the past!
When I 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 Java’s strongest points — you can just grab some source code from version X and it’s a really big surprise if it doesn’t compile in version 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 the 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 running is unknown. Even worse, whether a 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 also 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. The more (unspecified) threads, the more probability for deadlock.
Unconstrained behavior
A finalizer is just 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 10,000 threads. It can block its thread of execution. Have I already mentioned it can be run at an 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 hasn’t called super.finalize(), then the finalizer of the superclass won’t be called and its resources won’t be freed. Ooops!
JLS, JDK and JVM are more complex because of it
The 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 complicated 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 it is run.
Performance suffers
There are two-faced penalties for using finalizers. The first one is that garbage collection is slower because it (maybe) will schedule and run finalizers. The other one is that finalizers also execute on objects that are already properly cleaned up, for example, the close() method has already been called either manually or automatically at the end of a try-with-resources block.
Libraries that use finalization
I’m lucky enough that I never had to write a finalizer in production code. But even if you’re in the same situation, our applications always consist of code that someone else has written, whether it’s part of the JDK or some library that we use. And the reality in the trenches is that a lot of popular libraries use finalization — Guava, Apache Commons, Apache POI, Netty, Aspose and probably many others. As a matter of fact, if we have a .jar file with the library’s source code, we can check for the usage of finalization by using the [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 JDK’s 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 the AutoCloseable interface on all classes whose objects may hold resources
- use the Cleaner API for very long-lived objects
- add
--finalization=disabledand monitor the behavior of your app with regard to resource usage
Official JEP specification
If you want to dive deeply into the specification of this feature, you may find it in JEP 421.
Dear fellow developer, thank you for reading this article about the deprecation of finalization in JDK 18. Until next time, TheJavaGuy salutes you 👋!