Skip to content

Commit 83c4e25

Browse files
authored
Merge pull request #1086 from dwnusbaum/improve-cleanUpHeap
Ensure Java classes loaded by a Pipeline's `GroovyClassLoader` are cleaned up when the Pipeline completes
2 parents 2894bcd + 79738b7 commit 83c4e25

1 file changed

Lines changed: 40 additions & 20 deletions

File tree

plugin/src/main/java/org/jenkinsci/plugins/workflow/cps/CpsFlowExecution.java

Lines changed: 40 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,7 @@
8282
import java.io.IOException;
8383
import java.time.Duration;
8484
import java.util.ArrayList;
85+
import java.util.Arrays;
8586
import java.util.Collections;
8687
import java.util.List;
8788
import java.util.Map;
@@ -133,14 +134,14 @@
133134
import java.util.Collection;
134135
import java.util.Date;
135136
import java.util.HashSet;
136-
import java.util.Iterator;
137137
import java.util.LinkedHashMap;
138138
import java.util.Set;
139139
import java.util.TreeSet;
140140
import java.util.concurrent.CompletableFuture;
141141
import java.util.concurrent.RejectedExecutionException;
142142
import java.util.concurrent.TimeUnit;
143143
import java.util.concurrent.atomic.LongAdder;
144+
import java.util.function.Predicate;
144145
import java.util.stream.Collectors;
145146
import edu.umd.cs.findbugs.annotations.CheckForNull;
146147
import edu.umd.cs.findbugs.annotations.NonNull;
@@ -1448,20 +1449,23 @@ private static void cleanUpLoader(ClassLoader loader, Set<ClassLoader> encounter
14481449
cleanUpLoader(loader.getParent(), encounteredLoaders, encounteredClasses);
14491450
LOGGER.finer(() -> "found " + loader);
14501451
SerializableClassRegistry.getInstance().release(loader);
1451-
cleanUpGlobalClassValue(loader);
14521452
GroovyClassLoader gcl = (GroovyClassLoader) loader;
1453-
for (Class<?> clazz : gcl.getLoadedClasses()) {
1453+
Set<Class<?>> loadedClasses = new HashSet<>(Arrays.asList((Class<?>[]) gcl.getLoadedClasses()));
1454+
// GroovyClassLoader.getLoadedClasses() only returns Groovy classes, not Java classes loaded when using `@Grab`
1455+
cleanUpGlobalClassValue(loader, loadedClasses);
1456+
cleanUpClassHelperCache(loader, loadedClasses);
1457+
for (Class<?> clazz : loadedClasses) {
14541458
if (encounteredClasses.add(clazz)) {
14551459
LOGGER.finer(() -> "found " + clazz.getName());
1460+
// TODO: Do we also need to do a reverse lookup on the Introspector caches in case they have unique entries?
14561461
Introspector.flushFromCaches(clazz);
1457-
cleanUpClassHelperCache(clazz);
14581462
cleanUpLoader(clazz.getClassLoader(), encounteredLoaders, encounteredClasses);
14591463
}
14601464
}
14611465
gcl.clearCache();
14621466
}
14631467

1464-
private static void cleanUpGlobalClassValue(@NonNull ClassLoader loader) throws Exception {
1468+
private static void cleanUpGlobalClassValue(@NonNull ClassLoader loader, Set<Class<?>> loadedClasses) throws Exception {
14651469
Class<?> classInfoC = Class.forName("org.codehaus.groovy.reflection.ClassInfo");
14661470
// TODO switch to MethodHandle for speed
14671471
Field globalClassValueF = classInfoC.getDeclaredField("globalClassValue");
@@ -1491,31 +1495,47 @@ private static void cleanUpGlobalClassValue(@NonNull ClassLoader loader) throws
14911495
}
14921496
}
14931497
}
1494-
Iterator<Class<?>> it = toRemove.iterator();
1495-
while (it.hasNext()) {
1496-
Class<?> klazz = it.next();
1497-
ClassLoader encounteredLoader = klazz.getClassLoader();
1498-
if (encounteredLoader != loader) {
1499-
it.remove();
1500-
if (LOGGER.isLoggable(Level.FINEST)) {
1501-
LOGGER.finest(() -> "ignoring " + klazz + " with loader " + encounteredLoader);
1502-
}
1503-
}
1504-
}
1498+
toRemove.removeIf(isClassFromOtherClassLoader(loader));
15051499
LOGGER.fine(() -> "cleaning up " + toRemove + " associated with " + loader);
15061500
for (Class<?> klazz : toRemove) {
1501+
loadedClasses.add(klazz);
15071502
removeM.invoke(map, klazz);
15081503
}
15091504
}
15101505

1511-
private static void cleanUpClassHelperCache(@NonNull Class<?> clazz) throws Exception {
1506+
private static void cleanUpClassHelperCache(@NonNull ClassLoader loader, Set<Class<?>> loadedClasses) throws Exception {
15121507
Field classCacheF = Class.forName("org.codehaus.groovy.ast.ClassHelper$ClassHelperCache").getDeclaredField("classCache");
15131508
classCacheF.setAccessible(true);
15141509
Object classCache = classCacheF.get(null);
1515-
if (LOGGER.isLoggable(Level.FINER)) {
1516-
LOGGER.log(Level.FINER, "cleaning up {0} from ClassHelperCache? {1}", new Object[] {clazz.getName(), classCache.getClass().getMethod("get", Object.class).invoke(classCache, clazz) != null});
1510+
Class<?> classCacheC = classCache.getClass();
1511+
Collection entries = (Collection) classCacheC.getMethod("values").invoke(classCache);
1512+
Class<?> managedRefC = Class.forName("org.codehaus.groovy.util.ManagedReference");
1513+
var getRefM = managedRefC.getMethod("get");
1514+
List<Class<?>> toRemove = new ArrayList<>(); // not sure if it is safe against ConcurrentModificationException or not
1515+
for (Object entry : entries) {
1516+
Class<?> clazz = (Class<?>) getRefM.invoke(entry);
1517+
if (clazz != null) {
1518+
toRemove.add(clazz);
1519+
}
15171520
}
1518-
classCache.getClass().getMethod("remove", Object.class).invoke(classCache, clazz);
1521+
toRemove.removeIf(isClassFromOtherClassLoader(loader));
1522+
LOGGER.fine(() -> "cleaning up " + toRemove + " associated with " + loader);
1523+
Method removeM = classCache.getClass().getMethod("remove", Object.class);
1524+
for (Class<?> klazz : toRemove) {
1525+
loadedClasses.add(klazz);
1526+
removeM.invoke(classCache, klazz);
1527+
}
1528+
}
1529+
1530+
private static Predicate<Class<?>> isClassFromOtherClassLoader(ClassLoader loader) {
1531+
return klazz -> {
1532+
ClassLoader encounteredLoader = klazz.getClassLoader();
1533+
var irrelevant = encounteredLoader != loader;
1534+
if (irrelevant) {
1535+
LOGGER.finest(() -> "ignoring " + klazz + " with loader " + encounteredLoader);
1536+
}
1537+
return irrelevant;
1538+
};
15191539
}
15201540

15211541
synchronized @CheckForNull FlowHead getFirstHead() {

0 commit comments

Comments
 (0)