In my experience as a C++/Java/Android developer, I have come to learn that finalizers are almost always a bad idea, the only exception being the management of a "native peer" object needed by the java one to call C/C++ code through JNI.
I am aware of the JNI: Properly manage the lifetime of a java object question, but this question addresses the reasons not to use a finalizer anyway, neither for native peers. So it's a question/discussion on a confutation of the answers in the aforementioned question.
Joshua Bloch in his Effective Java explicitly lists this case as an exception to his famous advice on not using finalizers:
A second legitimate use of finalizers concerns objects with native peers. A native peer is a native object to which a normal object delegates via native methods. Because a native peer is not a normal object, the garbage collector doesn't know about it and can’t reclaim it when its Java peer is reclaimed. A finalizer is an appropriate vehicle for performing this task, assuming the native peer holds no critical resources. If the native peer holds resources that must be terminated promptly, the class should have an explicit termination method, as described above. The termination method should do whatever is required to free the critical resource. The termination method can be a native method, or it can invoke one.
(Also see "Why is the finalized method included in Java?" question on stackexchange)
Then I watched the really interesting How to manage native memory in Android talk at the Google I/O '17, where Hans Boehm actually advocates against using finalizers to manage native peers of a java object, also citing Effective Java as a reference. After quickly mentioning why explicit delete of the native peer or automatic closing based on scope might not be a viable alternative, he advises using java.lang.ref.PhantomReference
instead.
He makes some interesting points, but I am not completely convinced. I will try to run through some of them and state my doubts, hoping someone can shed further light on them.
Starting from this example:
class BinaryPoly { long mNativeHandle; // holds a c++ raw pointer private BinaryPoly(long nativeHandle) { mNativeHandle = nativeHandle; } private static native long nativeMultiply(long xCppPtr, long yCppPtr); BinaryPoly multiply(BinaryPoly other) { return new BinaryPoly ( nativeMultiply(mNativeHandle, other.mNativeHandler) ); } // … static native void nativeDelete (long cppPtr); protected void finalize() { nativeDelete(mNativeHandle); } }
Where a java class holds a reference to a native peer that gets deleted in the finalizer method, Bloch lists the shortcomings of such an approach.
Finalizers can run in arbitrary order
If two objects become unreachable, the finalizers actually run in arbitrary order, that includes the case when two objects who point to each others become unreachable at the same time they can be finalized in the wrong order, meaning that the second one to be finalized actually tries to access an object that’s already been finalized. [...] As a result of that you can get dangling pointers and see deallocated c++ objects [...]
And as an example:
class SomeClass { BinaryPoly mMyBinaryPoly: … // DEFINITELY DON’T DO THIS WITH CURRENT BinaryPoly! protected void finalize() { Log.v(“BPC”, “Dropped + … + myBinaryPoly.toString()); } }
Ok, but isn't this true also if myBinaryPoly is a pure Java object? As I understand it , the problem comes from operating on a possibly finalized object inside its owner's finalizer. In case we are only using the finalizer of an object to delete its own private native peer and not doing anything else, we should be fine, right?
Finalizer may be invoked while the native method is till running
By Java rules, but not currently on Android:
Object x’s finalizer may be invoked while one of x’s methods is still running, and accessing the native object.
Pseudo-code of what multiply()
gets compiled to is shown to explain this:
BinaryPoly multiply(BinaryPoly other) { long tmpx = this.mNativeHandle; // last use of “this” long tmpy = other.mNativeHandle; // last use of other BinaryPoly result = new BinaryPoly(); // GC happens here. “this” and “other” can be reclaimed and finalized. // tmpx and tmpy are still needed. But finalizer can delete tmpx and tmpy here! result.mNativeHandle = nativeMultiply(tmpx, tmpy) return result; }
This is scary, and I am actually relieved this doesn't happen on android, because what I understand is that this
and other
get garbage collected before they go out of scope! This is even weirder considering that this
is the object the method is called on, and that other
is the argument of the method, so they both should already "be alive" in the scope where the method is being called.
A quick workaround to this would be to call some dummy methods on both this
and other
(ugly!), or passing them to the native method (where we can then retrieve the mNativeHandle
and operate on it). And wait... this
is already by default one of the arguments of the native method!
JNIEXPORT void JNICALL Java_package_BinaryPoly_multiply (JNIEnv* env, jobject thiz, jlong xPtr, jlong yPtr) {}
How can this
be possibly garbage collected?
Finalizers can be deferred for too long
“For this to work correctly, if you run an application that allocates lots of native memory and relatively little java memory it may actually not be the case that the garbage collector runs promptly enough to actually invoke finalizers [...] so you actually may have to invoke System.gc() and System.runFinalization() occasionally, which is tricky to do [...]”
If the native peer is only seen by a single java object which it is tied to, isn’t this fact transparent to the rest of the system, and thus the GC should just have to manage the lifecycle of the Java object as it was a pure java one? There's clearly something I fail to see here.
Finalizers can actually extend the lifetime of the java object
[...] Sometimes finalizers actually extend the lifetime of the java object for another garbage collection cycle, which means for generational garbage collectors they may actually cause it to survive into the old generation and the lifetime may be greatly extended as a result of just having a finalizer.
I admit I don't really get what's the issue here and how it relates to having a native peer, I will make some research and possibly update the question :)
In conclusion
For now, I still believe that using a sort of RAII approach were the native peer is created in the java object's constructor and deleted in the finalize method is not actually dangerous, provided that:
Is there any other restriction that should be added, or there's really no way to ensure that a finalizer is safe even with all restrictions being respected?
“This method is inherently unsafe. It may result in finalizers being called on live objects while other threads are concurrently manipulating those objects, resulting in erratic behavior or deadlock.” So, in one way we can not guarantee the execution and in another way we the system in danger.
Finalize method in Java is an Object Class method that is used to perform cleanup activity before destroying any object. It is called by Garbage collector before destroying the object from memory. Finalize() method is called by default for every object before its deletion.
Since it is available for every java class, Garbage Collector can call the finalize() method on any java object. Why finalize() method is used? finalize() method releases system resources before the garbage collector runs for a specific object. JVM allows finalize() to be invoked only once per object.
finalization can easily break subclass/superclass relationships. there is no ordering among finalizers. a given object's finalize method is invoked at most once by the JVM, even if that object is "resurrected" there are no guarantees about timeliness of finalization or even that it will run at all.
finalize
and other approaches that use GC knowledge of objects lifetime have a couple of nuances:
It is possible to solve all of these issues with finalizers, but it requires a decent amount of code. Hans-J. Boehm has a great presentation which shows these issues and possible solutions.
To guarantee visibility, you have to synchronize your code, i.e., put operations with Release semantics in your regular methods, and an operation with Acquire semantics in your finalizer. For example:
volatile
at the end of each method + read of the same volatile
in a finalizer.keepAlive
implementation in Boehm's slides).To guarantee reachability (when it's not already guaranteed by the language specification), you may use:
nativeMultiply
is static
, therefore this
may be garbage-collected.Reference#reachabilityFence
from Java 9+.The difference between plain finalize
and PhantomReferences
is that the latter gives you way more control over the various aspects of finalization:
ReferenceQueues
).B
that must remain alive when A
is finalized as a field of PhantomReference
to A
;PhantomRefereces
strongly reachable until they are enqueued by GC.My own take is that one should release native objects as soon as you are done with them, in a deterministic fashion. As such, using scope to manage them is preferable to relying on the finalizer. You can use the finalizer to cleanup as a last resort, but, i would not use solely to manage the actual lifetime for the reasons you actually pointed out in your own question.
As such, let the finalizer be the final attempt, but not the first.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With