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

Paul King commented on GROOVY-12281:
------------------------------------

Thanks [~blackdrag] - the reverse memory pressure question was the important 
one: it found a real leak, now fixed, and the fix turned out to also be the 
answer to two of your inline comments.

*The reverse test.* I implemented it as you described (runtime stays alive, 
script loaders come and go, each script installs an EMC on a class it created, 
then memory pressure). Under the prototype as reviewed it *failed*: 0/20 
loaders collected, where the default mode collects 20/20. The cause was the 
global strong root set - its justifying comment ("everything in it dies with 
Groovy's own loader, which is the lifetime every ClassInfo has today") is true 
for immortal platform keys but wrong for collectible ones: a script class's 
today-lifetime is _its class's_ lifetime, because a plain ClassValue 
association is an ephemeron and dies with its key. The set extended every 
EMC-dirty script class (root -> ClassInfo -> strongMetaClass -> theClass -> 
loader) to the runtime's lifetime. Control runs isolated it exactly: clean 
script classes collected fine, and {{ClassInfo.remove()}} unrooted correctly.

The fix moves the pin _into the association_: the store's per-Class slot (a 
bootstrap {{AtomicReference}}) holds either a bootstrap {{SoftReference}} 
(reclaimable) or the value itself (pinned). The strong hold is then reachable 
only from the key class, so a pinned ClassInfo has exactly a plain ClassValue 
association's lifetime - immortal platform keys retain it (they must - the 
state is not reconstructible), dropped script classes release it with their 
loader. With the fix the reverse probe collects 20/20 under soft, same as the 
default, and it is now a scenario in {{ClassInfoSoftModeProbe}}. (Two 
probe-construction notes recorded in the assessment: the scenario is 
{{@CompileStatic}} and nulls its loop locals on purpose - indy call-site guards 
in a long-lived caller and stale frame slots retain script classes in *every* 
mode, receiver-side effects distinct from the association lifetime under test.)

One incidental find while running the matrix: {{groovy.use.classvalue=false}} 
on current master *also* fails the reverse scenario (0/20), while released 
5.0.6 and 6.0.0-beta-2 collect 20/20 - the reworked escape hatch's 
weak-key/strong-value map is not an ephemeron, so a value that reaches its own 
key revives it forever. That predates this PR; I'll file it separately.

*{{isSoftMode()}} coupling.* Agreed, and the leak fix supplied the right 
abstraction. The property {{ClassInfo}} actually depends on is "can a value be 
collected while its key class is still alive?" - false for the strong 
ClassValue *and* for the map (whose values die with their class), true only for 
soft. {{GroovyClassValue}} now exposes that as a capability 
({{valuesReclaimable()}}, default false) plus default-no-op {{pin}}/{{unpin}}; 
{{ClassInfo}} asks its own store, and {{isSoftMode()}} is gone from the 
factory's surface.

*"Always do it or simplify".* With {{pin}}/{{unpin}} on the interface, 
{{updateReclaimability()}} now runs unconditionally in every mode (no-ops 
elsewhere), and {{ClassInfo.remove()}} lost its store-specific branch entirely 
- the pin travels with the association. Always maintaining the previous *set* 
in all modes would have been wrong, for what it's worth: in {{=false}} mode it 
would add pinning that mode exists to avoid, and in default mode it was pure 
overhead.

*Architecture.* Agreed it was under-documented, and your layered reading is the 
intended one: {{SwitchPointInvalidator}} is the policy-free mechanism (one 
domain's SwitchPoint lifecycle plus the live registry), {{IndyInvalidation}} 
the policy layer (width, reasons, anchoring, per-Class continuity), and 
{{ClassInfo}} the only other supported consumer (it owns domain instances and 
performs the local operations directly). Java visibility can't enforce that 
across packages, so both classes now carry a "Layering" javadoc section naming 
the supported consumers and which guarantees exist at which level, and both are 
annotated {{@Internal}}. (Most of that machinery is GROOVY-12191's - the 
question really spans both changes.)

*Boolean for the mode.* Done - the factory stores parsed booleans and the mode 
string never leaves it.

*Hybrid.* It was never intended to land (measured and declined), so I've 
restructured the PR to make that explicit: the branch now carries only the 
master-bound payload - the soft commit rebuilt without the hybrid arm (factory 
is {{true}}/{{soft}}/{{false}} only), plus the review-response delta as a 
separate commit for your re-review, to be squashed at landing. The 
investigation artifacts (hybrid, guide quantification, assessment, spikes) 
moved to {{groovy12281-investigation}}. Apologies for the close/reopen noise on 
the PR - force-pushing the restructure to both repos briefly made head and base 
identical, which auto-closed it; the base branch is now parked at the fork 
point. Your inline comments will show as outdated because of the rebuild, but 
they're all addressed above.

*Soft vs weak.* Soft is deliberate: the canonical side map is weak-valued, so 
with weak slot references an otherwise-unreferenced ClassInfo would be cleared 
at every minor GC and platform-receiver ClassInfos would churn through 
recreation between collections - losing exactly the cache behaviour the mode 
exists to preserve. Soft approximates "collect only under pressure", which is 
also the ticket's acceptance criterion. Agreed it can stay; with the slot 
design the strength choice is localized in {{GroovyClassValueSoft}} and could 
become a policy parameter later if we ever want it.

State: unit tests now cover the capability and pin/unpin lifecycle (9), the 
probe has 8 scenarios including the reverse one, and the reflection package, 
metaclass suites and the real-GC stress test are green. The assessment is at v3 
with the full review record in section 12.


> 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
>         Attachments: GROOVY-12281-Assessment.pdf
>
>
> 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