PhantomReferences

Erik Osterlund erik.osterlund at oracle.com
Fri Jul 30 08:42:33 UTC 2021


Hi Hans,

> -----Original Message-----
> From: core-libs-dev <core-libs-dev-retn at openjdk.java.net> On Behalf Of
> Hans Boehm
> Sent: Friday, 30 July 2021 00:07
> To: core-libs-dev <core-libs-dev at openjdk.java.net>
> Subject: PhantomReferences
> 
> Here's another finalization-related issue, this time hopefully appropriate for
> this list. This was inspired by looking at the Ugawa, Jones, and Ritson paper
> from ISMM 2014, which I belatedly had a chance to look at.
> 
> The java.lang.ref spec says:
> 
> "An object is phantom reachable if it is neither strongly, softly, nor weakly
> reachable, it has been finalized, and some phantom reference refers to it."
> 
> It notably does not say that such an object must not be reachable from
> unfinalized objects.
> 
> I currently believe that:
> 
> 1) This spec is not as intended, in that it allows a PhantomReference to X to
> be enqueued while X is still actively being used. My understanding is that
> PhantomReferences were invented largely to make that impossible.

Agreed.

> 2) Real production implementations enforce a stronger requirement, which
> includes that the PhantomReference must not reachable from unfinalized
> objects with a nontrivial finalizer, which prevents this problem.

Correct.

> 3) The ISMM 2014 paper may have been confused by this, in that it seems to
> mirror the official spec rather than the usual implementation. It (surprisingly
> to me) does not appear to address the fact that implementations generally
> mark reachable objects in at least two stages:
> (1) Reachability from roots, and (2) Reachability from roots U unfinalized
> finalizable objects, where the result of the first phase is used to determine
> WeakReference clearing, while the result of the second phase determines
> PhantomReference clearing, and what to collect.
> 
> Am I correct?

I have also looked at that paper a bit, as well as had some discussions with the authors of that paper regarding finalizers and phantoms. They gave me a link to the code to help clarify what they have been up to, which was very helpful: https://github.com/rejones/sapphire/

Skimming through the code, I came to the conclusion that they do trace from finalizers like traditional collectors, between processing of weak and phantom references. So they are also computing transitive properties correctly AFAICT. However, there are some other issues with this though. The algorithm only allows you to load non-phantom strength references after marking terminates and before reference processing has executed, because the transitive closure of finalizers has yet to be computed before then. It is assumed that nobody would be interested in loading a phantom strength reference in this window of time. The paper says:

"Phantom reference objects are straightforward to process in an
OTF manner since there is no interaction between collector and
mutator: PantomReference.get() always returns null."

Now this was implemented in JikesRVM where AFAICT there is no class unloading implemented (a great source of phantom strength references in HotSpot). I believe the JNI spec was also a bit more vague at the time, and jweaks were indeed implemented with weak strength in their code. So since PhantomReference.get() just returns null, they could dodge most issues with phatom strength loads - a core assumption of the algorithm.
We also enjoy making our lives as painful as possible and since that paper was written, we added refersTo() to j.l.r.Reference, which also has to work on PhantomReference, with phantom strength semantics. That would be the nail in the coffin for such simplifications, as now PhantomReference itself indeed does have such mutator interactions, before concurrent reference processing has a chance to run.

ZGC also implements concurrent reference processing. Our approach is to during concurrent marking perform both the normal marking from strong roots, and finalizable marking from finalizers, both in the same concurrent marking phase. This requires an extra bit map to keep track of objects that are reachable from finalizers as well (but not through strong roots). The liveness can move around a bit during the marking (finalizable reachable can get transitively promoted to strongly reachable, but not the other way around), but once we marking terminates, we get a snapshot of this liveness information, that does not change. Then we have internally explicitly marked what strength each reference access has through an access API, such that weak references check the strong bitmap if the value will become concurrently cleared by the reference processor, while phantom references do the corresponding same thing using the other bitmap. That way, we can always when reading a phantom reference, lazily compute the same value that the concurrent reference processor would. We would be thrilled to remove that extra bit map we only need to deal with finalizers though. But that is what allows us to perform phantom loads right after marking terminates, and before concurrent reference processing has started, with the same semantics as-if it was all done STW.

> A scenario that I believe can fail according to the spec, but cannot and must
> not fail in real life, is the following, where F1 and F2 are objects with
> nontrivial finalizers, and P is the referent of a PhantomReference:
> 
> Consider F1 --> P,  where P has a PhantomReference referring to it, and
> <root> -> F2 -> null.  Then
> 
> 1) F1's finalizer runs and notionally P's (empty) finalizer runs. F1 modifies F2,
> so it gets a strong reference to P.
> 
> [ P has now been finalized. We have <root> -> F2 -> P ]
> 
> 2) <root> is cleared, making F2 unreachable.
> 
> [ P is not strongly, softly or weakly referenced, and has been finalized.
> Therefore P is phantom reachable. ]
> 
> 3) The PhantomReference to P is enqueued, resulting in running a Cleaner
> that e.g. deallocates native memory required by P.
> 
> 4) F2's finalizer runs and accesses P.
> 
> 5) Bad stuff.
> 
> Although this is arguably a weird corner case that is unlikely to occur
> frequently, I think it profoundly changes the algorithms used to implement
> this. "Has been finalized" is not the correct check; it's reachability from a not-
> yet-finalized object that matters. Hence the implementation must do a
> reachability analysis not technically required by the current spec.

Agreed.

> [ Just saying that in the spec probably doesn't work either. I suspect the fact
> that the finalizer is nontrivial also matters to get reasonable progress
> guarantees. Currently I think the spec doesn't have that notion, but it seems
> annoyingly essential. ]
> 
> Clearly, this problem goes away if you get rid of finalizers and merge
> {Phantom,Weak}References, which is presumably the intended end state,
> but not one that looks imminent to me.

Right.

/Erik

> Hans


More information about the core-libs-dev mailing list