[ 
https://issues.apache.org/jira/browse/GROOVY-12281?page=com.atlassian.jira.plugin.system.issuetabpanels:comment-tabpanel&focusedCommentId=18106071#comment-18106071
 ] 

Jochen Theodorou commented on GROOVY-12281:
-------------------------------------------

I went through the current `ClassInfo` / `CachedClass` / MetaClass / call-site 
machinery in more detail, and I think the assessment currently makes some 
assumptions that should be separated from what has actually been demonstrated.
h3. 1. `ClassInfo` is not intrinsically immortal

The current design does not make `ClassInfo` intrinsically immortal.

`globalClassValue` is the canonical `Class -> ClassInfo` association, but 
`ClassInfo` itself only weakly references the `Class`. The related runtime 
objects are also managed caches:
{code:java}
MetaClassImpl -> CachedClass -> ClassInfo
ClassInfo - -soft-> CachedClass
ClassInfo - -soft/weak-> MetaClass
ClassInfo - -weak-> Class
{code}
`ClassInfo` keeps its `CachedClass` through a soft `LazyReference`, while the 
non-strong MetaClass is held through a managed soft reference. Conversely, 
`CachedClass` has a strong `classInfo` reference and `MetaClassImpl` has a 
strong `CachedClass` reference.

This means that a `ClassInfo` can in principle become collectible when no 
external runtime structure keeps this chain alive. A new `ClassInfo` is 
therefore not conceptually impossible.

The existing code also deliberately supports removing the canonical association 
through `ClassInfo.remove(Class)`, for example for application/class-loader 
undeployment.
h3. 2. Legacy call sites can retain `ClassInfo`, but this makes the 
SoftReference argument less straightforward

The old `groovy-callsite` implementation is an important special case.

For example, `PojoMetaClassSite` stores both:
{code:java}
private final ClassInfo classInfo;
private final MetaClass metaClass;
private final int version;
{code}
and validates the cached site using the captured `ClassInfo` version.

The other legacy sites such as `ClassMetaClassGetPropertySite`, 
`MetaClassConstructorSite`, `PerInstancePojoMetaClassSite` and 
`StaticMetaClassSite` use the same general caching machinery.

Thus a live legacy call site can indeed keep an old `ClassInfo` strongly 
reachable.

However, this makes the SoftReference argument in §3.3 less straightforward 
than it currently appears.

If a live call site strongly retains `ClassInfo A`, then A is strongly 
reachable. GC-driven clearing of a `SoftReference<ClassInfo A>` therefore 
cannot simply produce:
{code:java}
ClassValue -> ClassInfo B
live call site -> ClassInfo A
{code}
The assessment needs a concrete reachable-object sequence showing how the 
canonical association can disappear while an old `ClassInfo` remains strongly 
reachable and can still be used by a live call site.

Merely showing that two `ClassInfo` objects can coexist is not sufficient. They 
can already coexist through explicit cache removal/recreation, so the relevant 
question is whether the proposed implementation introduces a new unsafe way for 
this to happen.
h3. 3. Indy and the legacy call-site implementation should be analysed 
separately

The newer indy machinery appears to obtain the current `ClassInfo` through 
`ClassInfo.getClassInfo(...)` in many places rather than capturing an old 
`ClassInfo` in the same way as the legacy call sites.

If that is correct, the identity/guard argument needs to be demonstrated 
independently for indy rather than generalized from the legacy call-site 
implementation.

In particular, the relevant question is whether an indy call site can continue 
to depend on an old `ClassInfo` after a new one has become canonical, and under 
what exact reference graph that can happen.
h3. 4. "Pristine" and DGM state need a more precise definition

I am also not convinced by the current use of "pristine".

The fact that DGM state is present in `ClassInfo` does not by itself establish 
that the `ClassInfo` is non-recomputable.

A trivial user class can have its `ClassInfo` created on demand and is still 
affected by DGM through `Object`. Therefore the relevant question is not 
whether DGM state is present, but whether that state can actually be 
reconstructed from surviving global/runtime state.

The assessment currently states that `dgmMetaMethods` and `newMetaMethods` are 
written during registry/extension-module registration and that there is no hook 
for recomputation. I think the missing step is to establish that the 
information required to reconstruct those arrays is genuinely unavailable after 
the `ClassInfo` has been collected.

In other words:
{code:java}
"the current implementation writes this state once"
{code}
is not equivalent to:
{code:java}
"the state is fundamentally non-reconstructible"
{code}
Those should be demonstrated separately.
h3. 5. `MetaClassImpl` and `CachedClass` are themselves deliberately 
replaceable/cache-like

The current `CachedClass` implementation also makes me hesitant to treat every 
object reachable from `ClassInfo` as identity-bearing state.

`CachedClass` contains numerous lazy/managed references for methods, fields, 
constructors, hierarchy, interfaces, superclass information, etc. These are 
clearly cache structures.

Likewise, `CachedClass.setNewMopMethods()` and `addNewMopMethods()` can discard 
an existing `MetaClassImpl`, update the MOP information, create a new 
`MetaClassImpl`, initialize it and install it again.

So the runtime already deliberately recreates MetaClass state while retaining 
the same `ClassInfo`.

This suggests that "state is recreated" and "state is identity-bound" need to 
be distinguished more carefully.
h3. 6. `indySwitchPointDomain` should be treated as a separate, recent design 
decision

The `indySwitchPointDomain` field is a relatively recent addition to 
`ClassInfo`.

I would therefore not use its existence as evidence that `ClassInfo` has 
historically had an absolute identity/lifetime invariant.

It may instead be a consequence of the current indy invalidation 
implementation, in which the switch-point domain was deliberately attached to a 
particular `ClassInfo` instance.

If so, an important design question is whether this state really has to be 
owned by `ClassInfo`, or whether it should instead be associated with a more 
stable per-`Class` identity.
h3. 7. Missing from the investigation

As I see it, the investigation has demonstrated that simply making `ClassInfo` 
softly collectible is not obviously semantics-preserving. However, I do not 
think it has yet demonstrated all of the claimed correctness failures.

In particular, I think the following questions still need explicit 
investigation:
 # For which `ClassInfo` state is recomputation genuinely impossible, as 
opposed to merely unsupported by the current implementation?

 # What strong references can keep an old `ClassInfo` alive after its cache 
association has disappeared?

 # Can the legacy call-site "old `ClassInfo`" scenario actually coincide with 
GC-driven removal of the `ClassValue` association?

 # Does the indy machinery have the same identity requirement, or does it 
resolve the current `ClassInfo`?

 # Is `indySwitchPointDomain` a fundamental requirement of `ClassInfo` 
identity, or a consequence of the newer invalidation implementation?

 # Can DGM/extension state be reconstructed from the registry/module state, or 
does some genuinely non-reconstructible information exist only in the old 
`ClassInfo`?

I think answering these questions would make the conclusion much stronger than 
the current blanket statement that all drop-and-recompute schemes are 
"structurally illegal".

At the moment, my conclusion is that the problem is clearly non-trivial, but 
that the current assessment has not yet closed the recomputation avenue to the 
extent claimed.

> Investigate mitigating class-loader pinning by ClassInfo.globalClassValue 
> under the default ClassValue implementation
> ---------------------------------------------------------------------------------------------------------------------
>
>                 Key: GROOVY-12281
>                 URL: https://issues.apache.org/jira/browse/GROOVY-12281
>             Project: Groovy
>          Issue Type: Improvement
>            Reporter: Paul King
>            Priority: Major
>
> Follow-up from review discussion on [PR 
> #2798|https://github.com/apache/groovy/pull/2798] (GROOVY-12142), preserving 
> the analysis from that review. A {{ClassValue}} realizes the chain _key class 
> → association → value → everything reachable from it_, and the association 
> lives as long as the key class 
> ([JDK-8136353|https://bugs.openjdk.org/browse/JDK-8136353], working as 
> intended). The {{ClassValue}} implementation class itself prevents no 
> unloading; what matters is the key's origin: a Groovy-loaded key class dies 
> with Groovy's loader (fine), but a JDK/platform key class is effectively 
> immortal, so any value reachable from it that was loaded by Groovy's loader 
> pins that loader forever. Indirection counts — a JDK-typed value (e.g. an 
> {{ArrayList}}) whose _elements_ are Groovy-loaded re-creates the pin one 
> level down.
> {{ClassInfo.globalClassValue}} is static with unknown keys, including 
> platform classes ({{String}} receives a {{ClassInfo}} in essentially every 
> Groovy program), so the default {{ClassValue}} path pins the loader; today's 
> only remedy is the global {{groovy.use.classvalue=false}} escape hatch, which 
> trades away the per-class fast path for all keys.
> The general mitigation — {{SoftReference}}-wrapped values with a 
> check-remove-recompute protocol — requires that recomputation be legal, and 
> for {{ClassInfo}} it is not in general: a {{ClassInfo}} can carry 
> non-recomputable state (modified metaclasses, category state), so a 
> softly-collected value could silently discard user metaclass customizations. 
> Approaches to investigate:
> * soft values while a {{ClassInfo}} is pristine, hardening the reference on 
> first mutation — only classes with customized metaclasses would then pin, a 
> far smaller set;
> * per-key policy: platform-loader keys get soft/map treatment, Groovy-loader 
> keys stay strong (the key-origin rule applied mechanically);
> * splitting {{ClassInfo}} into recomputable and stateful parts;
> * revisiting whether the map-based implementation should become the default, 
> with {{ClassValue}} as the opt-in fast path.
> Recompute cost and dispatch-path performance need measurement for any 
> candidate.



--
This message was sent by Atlassian Jira
(v8.20.10#820010)

Reply via email to