Hi Hans,

> -----Original Message-----
> From: core-libs-dev <core-libs-dev-r...@openjdk.java.net> On Behalf Of
> Hans Boehm
> Sent: Friday, 30 July 2021 00:07
> To: core-libs-dev <core-libs-dev@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

Reply via email to