The deprecation of Object: : finalize is an unusual step for the Java ecosystem. We dive deep into the Hotspot JVM to see how it works. We also compare to RAII and the Java 7, try-with-resources syntax. The article contrasts these very…
Material in this article has been adapted with permission from the forthcoming book “Optimizing Java” by Ben Evans and James Gough. The book is published by O’ Reilly and is available now in Early Release from O’Reilly and from Amazon.
InfoQ recently reported on the proposed deprecation of the method finalize () on the Object type. This method has been present since Java 1.0, but is widely regarded as a misfeature and a significant piece of legacy cruft in the platform. Nevertheless, the deprecation of a method present on Java’s Object type, would be a highly unusual step.
The finalize () mechanism is an attempt to provide automatic resource management, in a similar way to the RAII (Resource Acquisition Is Initialisation) pattern from C++ and similar languages. In that pattern, a destructor method (known as finalize () in Java) is provided, to enable automatic cleanup, and release of resources when the object is destroyed.
The basic use case for this is fairly simple – when an object is created, it takes ownership of some resource, and the object’s ownership of that resource persists for the lifetime of the object. Then, when the object dies, the ownership of the resource is automatically relinquished.
Let’s look at a quick simple C++ example that shows how to put an RAII wrapper around C-style file I/O. The core of this technique is that the object destructor method (denoted with a ~ at the start of a method named the same as the class) is used for cleanup:
The standard rationale for this approach is the observation that when the programmer opens a file handle it is all too easy to forget to call the close () function when it is no longer required, and so tying the resource ownership to the object lifetime makes sense. Getting rid of the object’s resources automatically then becomes the responsibility of the platform, not the programmer.
This promotes good design, especially when the only reason for a type to exist is to act as a “holder” of a resource such as a file or network socket.
In the Java world, the way that this is implemented is to use the JVM’s garbage collector as the subsystem that can definitively say that the object has died. If a finalize () method is provided on a type, then all objects of that type receive special treatment. An object that overrides finalize () is treated specially by the garbage collector.
One detail of Hotspot that we need to be aware of is that the VM has some special, implementation specific bytecodes in addition to the standard Java instructions. These specialist bytecodes are used to rewrite the standard ones in order to cope with certain special circumstances.
A complete list of the bytecode definitions, both standard Java and Hotspot special-case can be found here .
For our purposes, we care about the special case: return_register_finalizer instruction. This is needed because it is possible for JVMTI to rewrite bytecode for Object.. To precisely obey the standard, and register the finalizer at the correct time, it is necessary to identify the point at which Object. completes without the rewriting, and the special-case bytecode is used to mark this point.
The code for actually registering the object as needing finalization can be seen in the Hotspot interpreter. The file hotspot/src/cpu/x86/vm/c1_Runtime1_x86.cpp contains the core of the x86-specific port of the Hotspot interpreter. This has to be processor-specific because Hotspot makes heavy use of low-level assembly / machine code. The case register_finalizer_id contains the registration code.
Once the object has been registered as needing finalization, then instead of being immediately reclaimed during the garbage collection (GC) cycle, the object undergoes the following extended lifecycle:
Overall, this means that all objects to be finalized must first be recognized as unreachable via a GC mark, then finalized, and then GC must run again in order for the data to be collected. This means that finalizable objects persist for 1 extra GC cycle at least. In the case of objects that have become tenured, this can be a significant amount of time.
The mechanism has some extra complexity – more than we would like – as the queue-draining threads have to start secondary finalization threads that actually run the finalize () method. This is necessary to guard against the possibility that finalize () will block.
If finalize () were run on the queue-draining threads then a badly written finalize () could prevent the entire mechanism from working. To prevent this, we are forced to create a brand new thread for each object instance that requires finalization.
Not only that, but finalization threads must also ignore any exceptions that are thrown. This seems strange at first, but the finalization thread has no real way to handle the exception, and the original context that created the finalizable object is long gone. There is no meaningful way for any user code to be provided that could be aware of, or recover from the exception.
To clarify this, recall that an exception in Java provides a way to unwind the stack to find a method within the current execution thread that can recover from a non-fatal error. Seen in this light the restriction that finalization ignores exceptions is more understandable – the finalize () call happens on a totally different thread than the one that created or executed the object.
The majority of the finalization implementation is actually written in Java. The JVM has separate threads to perform finalization, that run at the same time as application threads for the majority of the required work. The core functionality is contained in the class java.lang.ref. Finalizer, a package-private class that is fairly simple to read.
The Finalizer class also provides some insight into how classes that are granted additional privilege by the runtime are granted that privilege. For example, it contains code like this:
Of course, in regular application code, this code would be nonsensical, as it creates an unused object. Unless the constructor has side-effects (usually considered a bad design decision in Java) , this would do nothing. In this case, the intent is to “hook” a new finalizable object.
The implementation of finalization also relies heavily on the FinalReference class. This is a subclass of java.lang.ref. Reference, a class that the runtime and VM handle specially. Like the more well-known soft and weak references, FinalReference objects get special treatment by the GC subsystem, comprising a mechanism that provides an interesting interaction between the VM and Java code (both platform and user) .
For all its technical interest the Java finalization implementation is fatally flawed, due to a mismatch with the memory management scheme of the platform.”
In the C++ case, memory is handled manually, with explicit lifetime management of objects under the explicit control of the programmer. This means that destruction can happen as the object is deleted, and so the acquisition and release of resources is directly tied to the lifetime of the object.
Java’s memory management subsystem is a garbage collector that runs as-needed, in response to running out of available memory to allocate. It therefore runs at non-deterministic intervals (if at all) and so the finalize () method is run only when the object is collected, at some unknown time.
If the finalize () mechanism is used to automatically release resources (such as file handles) , then there is no guarantee as to when (if ever) those resources will actually become available. This makes the finalize () mechanism fundamentally unsuitable for its stated purpose – automatic resource management.
To safely handle resource-owning objects, Java 7 introduced try-with-resources – a new syntax feature especially designed for handling resources automatically. This language level construct allows the a resource that is to be managed to be specified in parenthesis following the try keyword.
This must be an object construction clause – regular Java code is not permitted. The Java compiler will also check that the object being created is of a type that implements the AutoCloseable interface (which is a superinterface of Closeable that was introduced in Java 7 specifically for this purpose) .
The resource objects are thus in scope for the body of the try block, and at the end of the scope of the try block the close () method is called automatically, rather than making the developer remember to call the function. The invocation of the close () method behaves as if it were in a finally, and so is run even when an exception is thrown in the business logic.
The key point is that the lifetime of the local variable is now constrained to a single scope, so the automatic cleanup becomes tied to a scope and not to object lifetime. For example:
This innocent try-with-resources code is compiled into a fairly large amount of bytecode, which we can see by using the -p switch to javap to dump a decompiled form.
Despite having the same design intent, the finalization and try-with-resources are radically different from each other; finalization relies on assembly code deep in the interpreter to register objects for finalization and uses the garbage collector to kick off the cleanup using a reference queue and separate dedicated finalization threads. In particular, there is little if any trace of the mechanism in the bytecode, and the capability is provided by special mechanisms within the VM.
By contrast, try-with-resources is a purely compile-time mechanism that can be seen as syntactic sugar that simply produces regular bytecode and has no other special runtime behavior. The only possible visible effect of try-with-resources is that as it emits a large amount of automatic bytecode, it may impact the ability of the JIT compiler to effectively inline or compile methods that use it. This is not a reason to avoid it, however.
In summary, for resource management and in almost all other cases finalization is not fit for purpose. Finalization depends on GC, which is itself a non deterministic process, so that anything relying on finalization has no time guarantee as to when the resource will be released.
Whether or not the deprecation of finalization eventually leads to its removal, the advice remains the same; never write classes that override finalize () , and always refactor any classes you find in your own code that do.
The try-with-resources mechanism is the recommended best practice for implementing something similar to the C++ RAII pattern. It does limit the use of the pattern to block-scoped code, but this is due to the Java platform’s lack of low-level visibility into object lifetime. The Java developer must simply exercise discipline when dealing with resource objects and scope them as highly as possible – which is in itself a good design practice.
Ben Evans is co-founder of jClarity, a startup which delivers performance tools & services to help development & ops teams. He is an organizer for the LJC (London’s JUG) and a member of the JCP Executive Committee, helping define standards for the Java ecosystem. He is a Java Champion; 3-time JavaOne Rockstar Speaker; co-author of “The Well-Grounded Java Developer” & the new edition of “Java in a Nutshell” and a regular speaker on the Java platform, performance, concurrency, and related topics. Ben is available for speaking, teaching, writing and consultancy engagements – please contact for details.
© Source: https://www.infoq.com/articles/Finalize-Exiting-Java?utm_campaign=infoq_content&utm_source=infoq&utm_medium=feed&utm_term=global
All rights are reserved and belongs to a source media.