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:
- the native peer doesn't hold any critical resource (in that case there should be a separate method to release the resource, the native peer must only act as the the java object "counterpart" in the native realm)
- the native peer doesn't span threads or do weird concurrent stuff in its destructor (who would want to do that?!?)
- the native peer pointer is never shared outside the java object, only belongs to a single instance, and only accessed inside the java object's methods. On Android, a java object may access the native peer of another instance of the same class, right before calling a jni method accepting different native peers or, better, just passing the java objects to the native method itself
- the java object's finalizer only deletes its own native peer, and does nothing else
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?
Because function
nativeMultiply(long xCppPtr, long yCppPtr)
is static. If a native function is static, its second parameter isjclass
pointing to its class instead ofjobject
pointing tothis
. So in this casethis
is not one of the arguments.If it had not been static there would be only issue with the
other
object.