Daniel Sun created GROOVY-12223:
-----------------------------------
Summary: Introduce hidden class support
Key: GROOVY-12223
URL: https://issues.apache.org/jira/browse/GROOVY-12223
Project: Groovy
Issue Type: Improvement
Reporter: Daniel Sun
h2. Background
Groovy generates many short-lived synthetic classes at runtime, including:
* map/interface proxies ({{ProxyGeneratorAdapter}})
* reflection dispatch helpers ({{Reflector}} / {{ReflectorLoader}})
* per-class meta-method artifacts ({{ClassLoaderForClassArtifacts}})
Today these are defined with {{ClassLoader#defineClass}} as ordinary *named*
classes. That has three practical downsides:
# *Name pollution* — the synthetic types are discoverable via {{Class.forName}}
/ {{ClassLoader#loadClass}}.
# *Metaspace pressure* — their lifetime is tied to the defining class loader;
long-running applications that generate many artifacts retain them until the
loader itself is collected.
# *Access friction* — without nest membership, generated code cannot share
private access with the host class the way a true nestmate can.
JDK 15 introduced *hidden classes* ([JEP 371|https://openjdk.org/jeps/371]):
classes defined through {{Lookup#defineHiddenClass}} that are non-discoverable
by name, may join an access-control nest ({{NESTMATE}}), and may be unloaded
independently of the defining loader when not marked {{STRONG}}.
Groovy 6 requires JDK 17+, so the API is always present on supported runtimes.
h2. Proposal
Centralise hidden-class definition behind a single utility and prefer it for
the dynamic class-generation sites listed above, with a transparent fallback to
the existing {{ClassLoader#defineClass}} path.
h3. New API
{{org.apache.groovy.util.HiddenClassDefiner}} — the only call-site that invokes
{{Lookup#defineHiddenClass}}:
* {{defineHiddenClass(lookup, bytes, initialize, nestmate, strong)}} — full
control
* {{defineNestmateClass(lookup, bytes, initialize)}} — nestmate + weak
lifecycle (default for proxies / reflectors / artifacts)
* {{defineStrongHiddenClass(lookup, bytes, initialize)}} — non-discoverable,
loader-tied lifetime
* helpers: {{privateLookupIn(hostClass)}}, {{findConstructor(hiddenClass,
...parameterTypes)}}
Kill-switch (evaluated once at class-init for hot-path cost):
{noformat}
-Dgroovy.hidden.classes.disable=true
{noformat}
When disabled (or when private lookup / definition fails), callers fall back to
defining a normal visible class.
h3. Integration points
|| Site || Nest host || Preferred options || Fallback ||
| {{ClassLoaderForClassArtifacts#define}} | target (klazz) | nestmate, weak |
{{ClassLoader#defineClass}} + protection domain |
| {{ProxyGeneratorAdapter}} | non-{{Object}} superclass if present; else
{{ProxyGeneratorAdapter}} | nestmate, weak | {{InnerLoader#defineClass}} |
| {{ReflectorLoader#defineClass}} | {{Reflector}} | nestmate, weak |
{{ClassLoader#defineClass}} + protection domain |
Behaviour for callers of these generators is unchanged: proxies still implement
the requested interfaces, reflectors still dispatch, artifacts still construct.
The only observable differences when the hidden path succeeds are the synthetic
name form (contains {{/}}) and {{Class#isHidden() == true}}.
h2. Benefits
* Non-discoverable synthetic types (cleaner class-space / tooling view).
* Nestmate private access where the nest host can be opened for private lookup.
* Eager unloading of weak hidden classes reduces long-run metaspace retention
for short-lived proxies and artifacts.
* One policy / upgrade point if future JDKs add further {{Lookup.ClassOption}}
values.
h2. Compatibility
* Default-on when the JVM can obtain a full-privilege lookup for the chosen
nest host; silent fallback otherwise (e.g. sealed / unopened module packages).
* Opt-out: {{-Dgroovy.hidden.classes.disable=true}}.
* No public language-surface change; no change to successful proxy / reflector
/ artifact *behaviour*, only to how the {{Class}} is defined.
--
This message was sent by Atlassian Jira
(v8.20.10#820010)