1919package org .apache .groovy .runtime .indy ;
2020
2121import java .lang .invoke .SwitchPoint ;
22+ import java .lang .ref .Reference ;
23+ import java .lang .ref .ReferenceQueue ;
24+ import java .lang .ref .WeakReference ;
2225import java .util .List ;
2326import java .util .concurrent .ConcurrentHashMap ;
2427import java .util .concurrent .atomic .AtomicInteger ;
4750 * guard (GROOVY-12258, GROOVY-12259). Registration precedes publication, so
4851 * an empty (or entry-less) observation proves no guard chain holds a live
4952 * SwitchPoint that the observer could have needed to retire.
53+ * <p>
54+ * Registry entries hold their owning invalidator only weakly. When an owner
55+ * is collected without ever detaching (a discarded MetaClass or ClassInfo
56+ * domain in a long-lived, high-script-churn process that never bulk
57+ * invalidates), the orphaned SwitchPoint is <em>invalidated</em> — never
58+ * silently forgotten — either by the reaper pumped from allocation and drain,
59+ * or by the next bulk drain claiming it directly. Installed guards embed only
60+ * the SwitchPoint's internal invoker, which does not keep the SwitchPoint
61+ * object reachable, so the strong registry key is what keeps a
62+ * possibly-still-installed orphan guard retirable; invalidating it merely
63+ * forces straggler sites to re-link.
5064 *
5165 * @since 6.0.0
5266 */
@@ -61,20 +75,49 @@ public final class SwitchPointInvalidator {
6175 private static final SwitchPoint [] SINGLE_INVALIDATE_BUF = new SwitchPoint [1 ];
6276
6377 /**
64- * Registry of all live SwitchPoints, each mapped to its owning invalidator.
65- * Keyed by SwitchPoint — each has a single-use lifecycle (allocated once,
66- * detached once), so a removal can never clobber a successor's entry the
67- * way an invalidator-keyed registry could (ABA on re-allocation).
78+ * Registry of all live SwitchPoints, each mapped to a weak reference to
79+ * its owning invalidator. Keyed by SwitchPoint — each has a single-use
80+ * lifecycle (allocated once, detached once), so a removal can never
81+ * clobber a successor's entry the way an invalidator-keyed registry could
82+ * (ABA on re-allocation). Keys are strong on purpose: a guard chain keeps
83+ * only the SwitchPoint's internal invoker alive, so this entry is what
84+ * keeps an orphaned-but-installed guard retirable. Values are weak so a
85+ * dead domain's invalidator is collectable; {@link #reapOrphans()} then
86+ * invalidates (never just drops) the orphaned SwitchPoint.
6887 * <p>
6988 * Invariant: any SwitchPoint published in {@link #current} has an entry
7089 * here, established by registering <em>before</em> the publishing CAS.
7190 * Entries are removed only by the party that detaches the SwitchPoint
72- * (or by the allocator when its publishing CAS loses), so an entry whose
73- * SwitchPoint is not (yet) current is transient and self-resolving.
91+ * (or by the allocator when its publishing CAS loses, or by the party
92+ * that invalidates an orphan), so an entry whose SwitchPoint is not
93+ * (yet) current is transient and self-resolving.
7494 */
75- private static final ConcurrentHashMap <SwitchPoint , SwitchPointInvalidator > LIVE =
95+ private static final ConcurrentHashMap <SwitchPoint , OwnerRef > LIVE =
7696 new ConcurrentHashMap <>();
7797
98+ /**
99+ * Delivery queue for {@link OwnerRef}s whose invalidator was collected
100+ * while its SwitchPoint was still registered. Drained opportunistically
101+ * by {@link #reapOrphans()}; empty (and cost-free) unless domains die
102+ * undetached.
103+ */
104+ private static final ReferenceQueue <SwitchPointInvalidator > ORPHANS = new ReferenceQueue <>();
105+
106+ /**
107+ * Weak reference from a registered SwitchPoint to its owning invalidator.
108+ * Detach paths {@link Reference#clear() clear} the reference, which keeps
109+ * an explicitly handled entry from ever being enqueued; only owners that
110+ * die undetached reach {@link #ORPHANS}.
111+ */
112+ private static final class OwnerRef extends WeakReference <SwitchPointInvalidator > {
113+ final SwitchPoint sp ;
114+
115+ OwnerRef (final SwitchPoint sp , final SwitchPointInvalidator owner ) {
116+ super (owner , ORPHANS );
117+ this .sp = sp ;
118+ }
119+ }
120+
78121 /** {@code null} means no live switch point (lazy allocation on next get). */
79122 private final AtomicReference <SwitchPoint > current = new AtomicReference <>();
80123 private final AtomicInteger retirementCount = new AtomicInteger ();
@@ -94,20 +137,25 @@ public SwitchPointInvalidator() {
94137 * @return a live switch point (never {@code null})
95138 */
96139 public SwitchPoint getSwitchPoint () {
140+ // Allocation is the operation churn-heavy processes keep performing,
141+ // so it doubles as the reaper pump; a no-op while the queue is empty.
142+ reapOrphans ();
97143 for (;;) {
98144 SwitchPoint sp = current .get ();
99145 if (sp != null ) {
100146 return sp ;
101147 }
102148 SwitchPoint created = new SwitchPoint ();
149+ OwnerRef ref = new OwnerRef (created , this );
103150 // Register before publish: a bulk path that finds no entry is then
104151 // guaranteed the SwitchPoint was not visible to any guard, so
105152 // skipping it is safe.
106- LIVE .put (created , this );
153+ LIVE .put (created , ref );
107154 if (current .compareAndSet (null , created )) {
108155 return created ;
109156 }
110- LIVE .remove (created ); // lost the publishing race; never live
157+ ref .clear (); // lost the publishing race; never live
158+ LIVE .remove (created );
111159 }
112160 }
113161
@@ -130,7 +178,7 @@ public void invalidate() {
130178 public SwitchPoint detachLive () {
131179 SwitchPoint sp = current .getAndSet (null );
132180 if (sp != null ) {
133- LIVE . remove (sp );
181+ deregister (sp );
134182 retirementCount .incrementAndGet ();
135183 }
136184 return sp ;
@@ -148,13 +196,24 @@ public SwitchPoint detachLive() {
148196 */
149197 boolean detachIfCurrent (final SwitchPoint sp ) {
150198 if (current .compareAndSet (sp , null )) {
151- LIVE . remove (sp );
199+ deregister (sp );
152200 retirementCount .incrementAndGet ();
153201 return true ;
154202 }
155203 return false ;
156204 }
157205
206+ /**
207+ * Removes {@code sp}'s registry entry and clears its owner reference so an
208+ * explicitly detached SwitchPoint can never surface on {@link #ORPHANS}.
209+ */
210+ private static void deregister (final SwitchPoint sp ) {
211+ OwnerRef ref = LIVE .remove (sp );
212+ if (ref != null ) {
213+ ref .clear ();
214+ }
215+ }
216+
158217 /**
159218 * Returns how many live switch points have been retired.
160219 *
@@ -186,21 +245,50 @@ static boolean hasLiveSwitchPoints() {
186245 * Entries whose SwitchPoint is no longer (or not yet) current are left in
187246 * place: they are either about to be published (removing them would strand
188247 * a live SwitchPoint unregistered, invisible to all future drains) or are
189- * being removed by their detaching owner. The weakly consistent iteration
248+ * being removed by their detaching owner. Entries whose owner has been
249+ * collected are claimed here directly (the reaper may not have run yet)
250+ * and retired with the rest of the batch. The weakly consistent iteration
190251 * may miss a SwitchPoint published mid-drain — same window as the previous
191252 * all-classes walk; sites linking concurrently read the current category
192253 * state at link time.
193254 *
194255 * @param out destination list (must not be {@code null})
195256 */
196257 static void drainLive (final List <SwitchPoint > out ) {
197- LIVE .forEach ((sp , inv ) -> {
198- if (inv .detachIfCurrent (sp )) {
258+ reapOrphans ();
259+ LIVE .forEach ((sp , ref ) -> {
260+ SwitchPointInvalidator inv = ref .get ();
261+ if (inv == null ) {
262+ // Owner died undetached; exactly one claimant retires the orphan.
263+ if (LIVE .remove (sp , ref )) {
264+ out .add (sp );
265+ }
266+ } else if (inv .detachIfCurrent (sp )) {
199267 out .add (sp );
200268 }
201269 });
202270 }
203271
272+ /**
273+ * Retires SwitchPoints whose owning invalidator was collected while they
274+ * were still registered (e.g. discarded script-class domains in processes
275+ * that never bulk invalidate). Orphans are removed from the registry and
276+ * <em>invalidated</em>: an installed guard chain does not keep the
277+ * SwitchPoint object reachable, so a straggler site may still be guarding
278+ * on it — invalidation forces such sites to re-link, which is always safe.
279+ * No-op (a queue poll) unless owners have died undetached.
280+ */
281+ private static void reapOrphans () {
282+ for (Reference <? extends SwitchPointInvalidator > r ; (r = ORPHANS .poll ()) != null ; ) {
283+ OwnerRef ref = (OwnerRef ) r ;
284+ // Two-arg remove: lose gracefully to a concurrent drain claiming
285+ // the same orphan; exactly one party invalidates.
286+ if (LIVE .remove (ref .sp , ref )) {
287+ invalidateIfLive (ref .sp );
288+ }
289+ }
290+ }
291+
204292 /**
205293 * Number of currently registered live SwitchPoints (tests).
206294 *
@@ -220,6 +308,22 @@ static boolean isRegistered(final SwitchPoint sp) {
220308 return LIVE .containsKey (sp );
221309 }
222310
311+ /**
312+ * Clears {@code sp}'s owner reference while leaving its entry registered,
313+ * simulating an owner collected before queue delivery (tests).
314+ *
315+ * @param sp registered SwitchPoint
316+ * @return {@code true} if an entry was found and its reference cleared
317+ */
318+ static boolean clearOwnerRefForTesting (final SwitchPoint sp ) {
319+ OwnerRef ref = LIVE .get (sp );
320+ if (ref != null ) {
321+ ref .clear ();
322+ return true ;
323+ }
324+ return false ;
325+ }
326+
223327 /**
224328 * Invalidates {@code sp} when non-null and still valid.
225329 *
0 commit comments