On Tue, 10 Feb 2026 at 06:27, Jochen Theodorou <[email protected]> wrote:
>
>
>
> On 2/9/26 22:33, Chris Dennis wrote:
> > On Mon, 9 Feb 2026 at 15:04, Jochen Theodorou <[email protected]> wrote:
> >>
> >> On 02.02.26 17:50, Chris Dennis wrote:
> [...]
> > The lazy mechanism is working correctly - the problem is that the
> > lazily created instances are only softly-referenced which means the GC
> > can come along later and clean that reference, and then a newly
> > arriving thread will create an additional instance of CachedClass for
> > the same type. If/when those two instances then meet they will falsely
> > compare not-equal and the Groovy runtime will think they represent
> > different types (which they don't).
>
> Maybe what happens is that the class is ready to be collected, but then
> we use ClassInfo to get the instance, while at the same time we are
> creating a new instance? Just trying to figure out why two instances
> even exist.

The current mechanism I can see that allows you to end up with two
instances is because a ClassInfo softly references its CachedClass via
`ClassInfo.cachedClassRef` but CachedClass instances also softly
reference the CachedClass instances corresponding to the parameter
types of their methods via the `CachedClass.methods` field. When the
ClassInfo.cachedClassRef reference is cleared by the GC we are primed
for the construction of a new instance, but the old instance can still
be accessible via the `CachedClass.methods` field of any other type
with a method that takes that type as a parameter. In a more abstract
sense we can't rely on the current scheme to prevent multiple
instances from existing while we allow the instances to be
softly-reachable via paths involving more than one instance of
soft-reference, because those instances will not all be cleared at the
same time.

>
>
> > I think the fix here is to
> > implement (and use) an equals method for CachedClass which uses this
> > referential comparison but only as an optimistic fast path.
>
> that is a workaround for me though... well... it depends on the
> conditions we want those constructs to fullfill

To me this isn't a workaround, but an acceptance that we cannot
reliably maintain the 1:1 relationship between ClassInfo and
CachedClass - and that therefore the equality contract between them
cannot be a referential one.

>
> > (There is
> > another theoretical fix here where all accesses of a given CachedClass
> > are always mediated through a single SoftReference, which I think
> > would make the existing scheme safe, but I fear it would be overly
> > brittle).
>
> This sounds a lot like the soft reference will reference an instance,
> that only this soft reference will reference. Which would be bad

It's not bad... since the soft reference will not be cleared while the
referent is strongly referenced. So the CachedClass instance could not
be replaced while there was a strong reference to it that its future
equivalent could be compared with. I cannot think of an easy way of
preventing someone from breaking such a system though, (even if only
accidentally), so I think it's not worth the risk.

>
> >>> I'm attempting to narrow down how exactly this is happening and whether
> >>> the cause is OpenJ9 incorrectly clearing a soft reference to a strongly
> >>> reachable instance, or is due to Groovy missing one or more
> >>> reachabilityFences to prevent early clearing of these references.
> >>
> >> Can you verify other JVMs as well?
> >
> > I've not been able to reproduce this on anything other than OpenJ9 -
> > which I suspect is due to OpenJ9 being much more eager to clear
> > references. I'm pretty sure I have a valid mechanism through which it
> > can happen though - it just seems to be impossible to make Hotspot
> > trigger it (so far).
>
> Haven't worked with the eclipse/IBM JVm for many years (since Java 9 or
> so), but I do remember having regularily trouble with the references
> stuff on there.
>
> bye Jochen
>

Chris

Reply via email to