Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -20,16 +20,10 @@

import org.antlr.v4.runtime.atn.ATN;
import org.apache.groovy.util.SystemUtil;
import org.codehaus.groovy.runtime.DefaultGroovyMethods;

import java.lang.invoke.MethodHandles;
import java.lang.ref.Reference;
import java.lang.ref.ReferenceQueue;
import java.lang.ref.SoftReference;
import java.util.concurrent.atomic.AtomicLong;
import java.util.concurrent.locks.ReentrantReadWriteLock;
import java.util.logging.Level;
import java.util.logging.Logger;

/**
* Manage ATN to avoid memory leak
Expand All @@ -40,8 +34,7 @@ public abstract class AtnManager {
public static final ReentrantReadWriteLock.ReadLock READ_LOCK = RRWL.readLock();
private static final String DFA_CACHE_THRESHOLD_OPT = "groovy.antlr4.cache.threshold";
private static final long DFA_CACHE_THRESHOLD;
private final ReferenceQueue<AtnWrapper> atnWrapperReferenceQueue = new ReferenceQueue<>();
private AtnWrapperSoftReference atnWrapperSoftReference;
private SoftReference<AtnWrapper> atnWrapperSoftReference;

static {
long t = SystemUtil.getLongSafe(DFA_CACHE_THRESHOLD_OPT, 0L);
Expand All @@ -52,26 +45,6 @@ public abstract class AtnManager {
DFA_CACHE_THRESHOLD = t;
}

{
Thread cleanupThread = new Thread(() -> {
while (true) {
try {
Reference<? extends AtnWrapper> reference = atnWrapperReferenceQueue.remove();
if (reference instanceof AtnWrapperSoftReference atnWrapperSoftReference && shouldClearDfaCache() && isSmartCleanupEnabled()) {
atnWrapperSoftReference.getAtnManager().getAtnWrapper(false).clearDFA();
}
} catch (Throwable t) {
Logger logger = Logger.getLogger(MethodHandles.lookup().lookupClass().getName());
if (logger.isLoggable(Level.WARNING)) {
logger.warning(DefaultGroovyMethods.asString(t));
}
}
}
}, "DFA-cache-cleaner[" + this.getClass().getSimpleName() + "]");
cleanupThread.setDaemon(true);
cleanupThread.start();
}

private static boolean isSmartCleanupEnabled() {
return 0 == DFA_CACHE_THRESHOLD;
}
Expand All @@ -93,9 +66,22 @@ private AtnWrapper getAtnWrapper(final boolean useSoftRef) {

AtnWrapper atnWrapper;
synchronized (this) {
if (null == atnWrapperSoftReference || null == (atnWrapper = atnWrapperSoftReference.get())) {
if (null == atnWrapperSoftReference) {
atnWrapper = createAtnWrapper();
atnWrapperSoftReference = new SoftReference<>(atnWrapper);
} else if (null == (atnWrapper = atnWrapperSoftReference.get())) {
// The softly referenced wrapper is a GC canary: its collection
// signals memory pressure, so drop the shared DFA cache along
// with allocating the replacement. Detected here on the parse
// path rather than by a reference-queue thread — a cleanup
// thread per manager can never terminate and so pins the
// defining class loader for the life of the JVM, leaking every
// container redeployment (GROOVY-12142).
atnWrapper = createAtnWrapper();
atnWrapperSoftReference = new AtnWrapperSoftReference(atnWrapper, this, atnWrapperReferenceQueue);
if (shouldClearDfaCache() && isSmartCleanupEnabled()) {
atnWrapper.clearDFA();
}
atnWrapperSoftReference = new SoftReference<>(atnWrapper);
}
}
return atnWrapper;
Expand Down Expand Up @@ -134,17 +120,4 @@ public void clearDFA() {
}
}
}

private static class AtnWrapperSoftReference extends SoftReference<AtnWrapper> {
private final AtnManager atnManager;

public AtnWrapperSoftReference(AtnWrapper referent, AtnManager atnManager, ReferenceQueue<? super AtnWrapper> q) {
super(referent, q);
this.atnManager = atnManager;
}

public AtnManager getAtnManager() {
return atnManager;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -18,11 +18,24 @@
*/
package org.codehaus.groovy.reflection;

import org.apache.groovy.util.SystemUtil;
import org.codehaus.groovy.reflection.GroovyClassValue.ComputeValue;
import org.codehaus.groovy.reflection.v7.GroovyClassValueJava7;

class GroovyClassValueFactory {
/**
* Escape hatch for deployments where {@code java.lang.ClassValue} pins
* class loaders (JDK-8136353): associations on immortal classes never
* release their value's loader, leaking every Groovy copy a container
* deploys and undeploys (GROOVY-12142). Set
* {@code -Dgroovy.use.classvalue=false} at JVM startup to use a weak-key
* map instead; the default remains ClassValue for its per-Class fast path.
*/
private static final boolean USE_CLASSVALUE = Boolean.parseBoolean(SystemUtil.getSystemPropertySafe("groovy.use.classvalue", "true"));

public static <T> GroovyClassValue<T> createGroovyClassValue(ComputeValue<T> computeValue) {
return new GroovyClassValueJava7<>(computeValue);
return (USE_CLASSVALUE)
? new GroovyClassValueJava7<>(computeValue)
: new GroovyClassValueMapBased<>(computeValue);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.codehaus.groovy.reflection;

import org.apache.groovy.util.concurrent.ManagedIdentityConcurrentMap;

/**
* Map-based {@link GroovyClassValue} used when {@code java.lang.ClassValue}
* must be avoided: due to JDK-8136353, a ClassValue association on an
* immortal class (for example a bootstrap class such as {@code String})
* retains its value — and with it the value's class loader — forever, which
* leaks every Groovy copy deployed and undeployed by a container
* (GROOVY-12142). Class keys are held weakly with identity semantics, so
* associations die with their class and never pin a loader.
* <p>
* The trade-off is a hash lookup per access instead of ClassValue's
* per-{@code Class} fast path; selection is therefore opt-in via
* {@code -Dgroovy.use.classvalue=false}.
*
* @param <T> the value type
*/
class GroovyClassValueMapBased<T> implements GroovyClassValue<T> {

private final ManagedIdentityConcurrentMap<Class<?>, T> map = new ManagedIdentityConcurrentMap<>();
private final ComputeValue<T> computeValue;

GroovyClassValueMapBased(final ComputeValue<T> computeValue) {
this.computeValue = computeValue;
}

@Override
public T get(final Class<?> type) {
return map.applyIfAbsent(type, computeValue::computeValue);
}

@Override
public void remove(final Class<?> type) {
map.remove(type);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,6 @@
package org.codehaus.groovy.vmplugin.v8;

import org.apache.groovy.util.SystemUtil;
import org.codehaus.groovy.runtime.DefaultGroovyMethods;
import org.codehaus.groovy.runtime.memoize.MemoizeCache;

import java.io.Serial;
Expand All @@ -30,11 +29,7 @@
import java.lang.ref.SoftReference;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.atomic.AtomicLong;
import java.util.logging.Level;
import java.util.logging.Logger;

/**
* Represents a cacheable call site, which can reduce the cost of resolving methods
Expand All @@ -43,15 +38,6 @@
*/
public class CacheableCallSite extends MutableCallSite {
private static final int CACHE_SIZE = SystemUtil.getIntegerSafe("groovy.indy.callsite.cache.size", 8);
/**
* When {@code true}, stale (GC-cleared) cache entries are swept inline on the calling thread
* instead of being handed to the background {@code PIC-Cleaner} daemon, which is then never
* started. Cleanup behaviour is otherwise identical, since the caller already holds the
* {@code lruCache} monitor at both call sites. This is primarily useful for tests and tools
* such as deterministic concurrency checkers, which flag the perpetually parked daemon thread
* as a (false-positive) deadlock. Defaults to {@code false} (GROOVY-12092).
*/
private static final boolean CLEAN_INLINE = SystemUtil.getBooleanSafe("groovy.indy.callsite.cleaner.inline");
private static final float LOAD_FACTOR = 0.75f;
private static final int INITIAL_CAPACITY = (int) Math.ceil(CACHE_SIZE / LOAD_FACTOR) + 1;
private final MethodHandles.Lookup lookup;
Expand Down Expand Up @@ -187,17 +173,19 @@ public MethodHandleWrapper put(String name, MethodHandleWrapper mhw) {
}
}

/**
* Sweeps GC-cleared cache entries inline; both call sites already hold the
* {@code lruCache} monitor and the cache is bounded by {@code CACHE_SIZE},
* so the sweep is trivial. A background cleaner thread (the former
* {@code PIC-Cleaner} daemon) must not be used here: a never-terminating
* thread started from a static initializer keeps its defining class loader
* reachable for the life of the JVM — and captures the creating context's
* protection domains — leaking every container redeployment
* (GROOVY-12142). Inline sweeping also keeps deterministic concurrency
* checkers happy (GROOVY-12092).
*/
private void removeAllStaleEntriesOfLruCache() {
if (CLEAN_INLINE) {
// both call sites already hold the lruCache monitor
lruCache.values().removeIf(v -> null == v.get());
return;
}
CACHE_CLEANER_QUEUE.offer(() -> {
synchronized (lruCache) {
lruCache.values().removeIf(v -> null == v.get());
}
});
lruCache.values().removeIf(v -> null == v.get());
}

/**
Expand Down Expand Up @@ -271,23 +259,4 @@ public MethodHandles.Lookup getLookup() {
return lookup;
}

private static final BlockingQueue<Runnable> CACHE_CLEANER_QUEUE = new LinkedBlockingQueue<>();
static {
if (!CLEAN_INLINE) {
Thread cacheCleaner = new Thread(() -> {
while (true) {
try {
CACHE_CLEANER_QUEUE.take().run();
} catch (Throwable ignore) {
Logger logger = Logger.getLogger(MethodHandles.lookup().lookupClass().getName());
if (logger.isLoggable(Level.FINEST)) {
logger.finest(DefaultGroovyMethods.asString(ignore));
}
}
}
}, "PIC-Cleaner");
cacheCleaner.setDaemon(true);
cacheCleaner.start();
}
}
}
37 changes: 37 additions & 0 deletions src/spec/doc/guide-integrating.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -378,6 +378,43 @@ stub generation is done, for the joint compiler.

However, overriding `CompilationUnit` is not recommended and should only be done if no other standard solution works.

== Class loader release in managed environments

Applications that load and discard Groovy repeatedly — a web container performing (parallel)
redeployments with Groovy inside each web application, plugin systems, or any host that expects
class loaders to be garbage collected — need one extra consideration.

Groovy associates runtime metadata (`ClassInfo`) with every class it dispatches on, including
JDK classes such as `String`. By default those associations use `java.lang.ClassValue`, which
gives the fastest possible lookup, but a long-standing JVM issue
(https://bugs.openjdk.org/browse/JDK-8136353[JDK-8136353]) prevents such associations on
long-lived classes from ever releasing their value — and with it the Groovy class loader the
value belongs to. In a container this shows up as metaspace growth on every redeployment, even
after the old application is undeployed.

Two supported measures release the class loader; either one suffices:

* Start the JVM with `-Dgroovy.use.classvalue=false`. Groovy then stores the associations in a
weak-key map instead of `ClassValue`. The trade-off is a hash lookup where `ClassValue` has a
per-class fast path; for most applications the difference is not measurable.
* Clean up explicitly when the application is discarded (for example from
`ServletContextListener#contextDestroyed`):
+
[source,groovy]
----
import org.codehaus.groovy.reflection.ClassInfo

for (ClassInfo ci : ClassInfo.getAllClassInfo()) {
Class<?> c = ci.theClass
if (c != null) ClassInfo.remove(c)
}
----

NOTE: Groovy itself starts no non-terminating background threads, so no thread of Groovy's will
pin the class loader. Threads started by user code (including via `Thread.start` from scripts, or
executors created by scripts) capture the creating context and must be shut down by the
application as usual.

include::../../../subprojects/groovy-jsr223/src/spec/doc/_integrating-jsr223.adoc[leveloffset=+1]


Loading
Loading