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

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.

Leave a Comment

tech