Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Should Java finalizer really be avoided also for native peer objects lifecycle management?

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?

like image 590
athos Avatar asked May 21 '17 09:05

athos


People also ask

Why should you avoid the finalize () method in the object class?

“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.

What is true of a finalizer in Java?

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.

Why need finalizer methods finalize ()?

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.

Why Finalize method is deprecated in Java?

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.


2 Answers

finalize and other approaches that use GC knowledge of objects lifetime have a couple of nuances:

  • visibility: do you guarantee that all the writes methods of object o made are visible to the finalizer (i.e., there is a happens-before relationship between the last action on object o and the code performing finalization)?
  • reachability: how do you guarantee, that an object o isn't destroyed prematurely (e.g., whilst one of its methods is running), which is allowed by the JLS? It does happen and cause crashes.
  • ordering: can you enforce a certain order in which objects are finalized?
  • termination: do you need to destroy all the objects when your app terminates?
  • throughput: GC-based approaches offer significantly smaller deallocation throughput than the deterministic approach.

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:

  • A store in a volatile at the end of each method + read of the same volatile in a finalizer.
  • Release lock on the object at the end of each method + acquire the lock at the beginning of a finalizer (see keepAlive implementation in Boehm's slides).

To guarantee reachability (when it's not already guaranteed by the language specification), you may use:

  • Synchronization approaches described above also ensure reachability.
  • Pass references to the objects that must remain reachable (= non-finalizable) as arguments to native methods. In the talk you reference, 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:

  • Can have multiple queues receiving phantom refs and pick a thread performing finalization for each of them.
  • Can finalize in the same thread that did allocation (e.g., thread local ReferenceQueues).
  • Easier to enforce ordering: keep a strong reference to an object B that must remain alive when A is finalized as a field of PhantomReference to A;
  • Easier to implement safe termination, as you must keep PhantomRefereces strongly reachable until they are enqueued by GC.
like image 174
Dmitry Timofeev Avatar answered Oct 03 '22 05:10

Dmitry Timofeev


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.

like image 41
cineam mispelt Avatar answered Oct 03 '22 04:10

cineam mispelt