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 samevolatile
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
isstatic
, thereforethis
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 whenA
is finalized as a field ofPhantomReference
toA
; - Easier to implement safe termination, as you must keep
PhantomRefereces
strongly reachable until they are enqueued by GC.