Skip to content

Commit 4214f23

Browse files
author
Dmytro Ukhlov
committed
JENKINS-75675: Refactor classloader logic in order to reduce memory consumption
Add non-locking DelegatingClassloader base implementation and use it for classloaders which delegate functionality to other classloaders
1 parent 4f6921b commit 4214f23

File tree

6 files changed

+172
-54
lines changed

6 files changed

+172
-54
lines changed

core/src/main/java/hudson/ClassicPluginStrategy.java

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@
3333
import hudson.model.Hudson;
3434
import hudson.util.CyclicGraphDetector;
3535
import hudson.util.CyclicGraphDetector.CycleDetectedException;
36+
import hudson.util.DelegatingClassLoader;
3637
import hudson.util.IOUtils;
3738
import hudson.util.MaskingClassLoader;
3839
import java.io.File;
@@ -559,7 +560,7 @@ private static void unzipExceptClasses(File archive, File destDir, Project prj)
559560
/**
560561
* Used to load classes from dependency plugins.
561562
*/
562-
static final class DependencyClassLoader extends ClassLoader {
563+
static final class DependencyClassLoader extends DelegatingClassLoader {
563564
/**
564565
* This classloader is created for this plugin. Useful during debugging.
565566
*/
@@ -574,10 +575,6 @@ static final class DependencyClassLoader extends ClassLoader {
574575
*/
575576
private volatile List<PluginWrapper> transitiveDependencies;
576577

577-
static {
578-
registerAsParallelCapable();
579-
}
580-
581578
DependencyClassLoader(ClassLoader parent, File archive, List<Dependency> dependencies, PluginManager pluginManager) {
582579
super("dependency ClassLoader for " + archive.getPath(), parent);
583580
this._for = archive;

core/src/main/java/hudson/PluginManager.java

Lines changed: 42 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -58,8 +58,10 @@
5858
import hudson.security.ACLContext;
5959
import hudson.security.Permission;
6060
import hudson.security.PermissionScope;
61+
import hudson.util.CachingClassLoader;
6162
import hudson.util.CyclicGraphDetector;
6263
import hudson.util.CyclicGraphDetector.CycleDetectedException;
64+
import hudson.util.DelegatingClassLoader;
6365
import hudson.util.FormValidation;
6466
import hudson.util.PersistedList;
6567
import hudson.util.Retrier;
@@ -106,13 +108,11 @@
106108
import java.util.Locale;
107109
import java.util.Map;
108110
import java.util.Objects;
109-
import java.util.Optional;
110111
import java.util.ServiceLoader;
111112
import java.util.Set;
112113
import java.util.TreeMap;
113114
import java.util.UUID;
114115
import java.util.concurrent.ConcurrentHashMap;
115-
import java.util.concurrent.ConcurrentMap;
116116
import java.util.concurrent.CopyOnWriteArrayList;
117117
import java.util.concurrent.Future;
118118
import java.util.function.Supplier;
@@ -356,7 +356,19 @@ PluginManager doCreate(@NonNull Class<? extends PluginManager> klass,
356356
*/
357357
// implementation is minimal --- just enough to run XStream
358358
// and load plugin-contributed classes.
359-
public final ClassLoader uberClassLoader = new UberClassLoader(activePlugins);
359+
public final ClassLoader uberClassLoader = new CachingClassLoader("Caching UberClassLoader",
360+
new UberClassLoader(activePlugins)) {
361+
@Override
362+
protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
363+
// basic check before use cache-based loading
364+
if (name.startsWith("SimpleTemplateScript") || // cf. groovy.text.SimpleTemplateEngine
365+
name.startsWith("groovy.tmp.")) {
366+
throw new ClassNotFoundException("ignoring " + name);
367+
}
368+
369+
return super.loadClass(name, resolve);
370+
}
371+
};
360372

361373
/**
362374
* Once plugin is uploaded, this flag becomes true.
@@ -538,7 +550,7 @@ protected void reactOnCycle(PluginWrapper q, List<PluginWrapper> cycle) {
538550
for (PluginWrapper p : cgd.getSorted()) {
539551
if (p.isActive()) {
540552
activePlugins.add(p);
541-
((UberClassLoader) uberClassLoader).clearCacheMisses();
553+
((CachingClassLoader) uberClassLoader).clearCacheMisses();
542554
}
543555
}
544556
} catch (CycleDetectedException e) { // TODO this should be impossible, since we override reactOnCycle to not throw the exception
@@ -660,7 +672,6 @@ void considerDetachedPlugin(String shortName, String source) {
660672
} catch (IOException e) {
661673
failedPlugins.add(new FailedPlugin(arc.getName(), e));
662674
}
663-
664675
}
665676
}
666677

@@ -973,7 +984,7 @@ public void dynamicLoad(File arc, boolean removeExisting, @CheckForNull List<Plu
973984
plugins.add(p);
974985
if (p.isActive()) {
975986
activePlugins.add(p);
976-
((UberClassLoader) uberClassLoader).clearCacheMisses();
987+
((CachingClassLoader) uberClassLoader).clearCacheMisses();
977988
}
978989

979990
// TODO antimodular; perhaps should have a PluginListener to complement ExtensionListListener?
@@ -2396,43 +2407,39 @@ public <T> T of(String key, Class<T> type, Supplier<T> func) {
23962407
/**
23972408
* {@link ClassLoader} that can see all plugins.
23982409
*/
2399-
public static final class UberClassLoader extends ClassLoader {
2410+
public static final class UberClassLoader extends DelegatingClassLoader {
24002411
private final List<PluginWrapper> activePlugins;
24012412

2402-
/** Cache of loaded, or known to be unloadable, classes. */
2403-
private final ConcurrentMap<String, Optional<Class<?>>> loaded = new ConcurrentHashMap<>();
2404-
2405-
static {
2406-
registerAsParallelCapable();
2407-
}
2408-
24092413
public UberClassLoader(List<PluginWrapper> activePlugins) {
24102414
super("UberClassLoader", PluginManager.class.getClassLoader());
24112415
this.activePlugins = activePlugins;
24122416
}
24132417

24142418
@Override
2415-
protected Class<?> findClass(String name) throws ClassNotFoundException {
2416-
if (name.startsWith("SimpleTemplateScript")) { // cf. groovy.text.SimpleTemplateEngine
2417-
throw new ClassNotFoundException("ignoring " + name);
2419+
protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
2420+
if (getResource(name.replace('.', '/') + ".class") == null &&
2421+
!name.equals("java.lang.$JaCoCo")) { // Add support for loading of JaCoCo dynamic instrumentation classes
2422+
throw new ClassNotFoundException(name);
24182423
}
2419-
return loaded.computeIfAbsent(name, this::computeValue).orElseThrow(() -> new ClassNotFoundException(name));
2424+
2425+
return super.loadClass(name, resolve);
24202426
}
24212427

2422-
private Optional<Class<?>> computeValue(String name) {
2428+
@Override
2429+
protected Class<?> findClass(String name) throws ClassNotFoundException {
24232430
for (PluginWrapper p : activePlugins) {
24242431
try {
24252432
if (FAST_LOOKUP) {
2426-
return Optional.of(ClassLoaderReflectionToolkit.loadClass(p.classLoader, name));
2433+
return ClassLoaderReflectionToolkit.loadClass(p.classLoader, name);
24272434
} else {
2428-
return Optional.of(p.classLoader.loadClass(name));
2435+
return p.classLoader.loadClass(name);
24292436
}
24302437
} catch (ClassNotFoundException e) {
24312438
// Not found. Try the next class loader.
24322439
}
24332440
}
2434-
// Not found in any of the class loaders. Delegate.
2435-
return Optional.empty();
2441+
2442+
throw new ClassNotFoundException(name);
24362443
}
24372444

24382445
@Override
@@ -2464,31 +2471,28 @@ protected Enumeration<URL> findResources(String name) throws IOException {
24642471
return Collections.enumeration(resources);
24652472
}
24662473

2467-
void clearCacheMisses() {
2468-
loaded.values().removeIf(Optional::isEmpty);
2469-
}
24702474

24712475
@Override
24722476
public String toString() {
24732477
// only for debugging purpose
24742478
return "classLoader " + getClass().getName();
24752479
}
2480+
}
24762481

2477-
// TODO Remove this once we require post 2024-07 remoting minimum version and deleted ClassLoaderProxy#fetchJar(URL)
2478-
@SuppressFBWarnings(
2479-
value = "DMI_COLLECTION_OF_URLS",
2480-
justification = "All URLs point to local files, so no DNS lookup.")
2481-
@Restricted(NoExternalUse.class)
2482-
public boolean isPluginJar(URL jarUrl) {
2483-
for (PluginWrapper plugin : activePlugins) {
2484-
if (plugin.classLoader instanceof URLClassLoader) {
2485-
if (Set.of(((URLClassLoader) plugin.classLoader).getURLs()).contains(jarUrl)) {
2486-
return true;
2487-
}
2482+
// TODO Remove this once we require post 2024-07 remoting minimum version and deleted ClassLoaderProxy#fetchJar(URL)
2483+
@SuppressFBWarnings(
2484+
value = "DMI_COLLECTION_OF_URLS",
2485+
justification = "All URLs point to local files, so no DNS lookup.")
2486+
@Restricted(NoExternalUse.class)
2487+
public boolean isPluginJar(URL jarUrl) {
2488+
for (PluginWrapper plugin : activePlugins) {
2489+
if (plugin.classLoader instanceof URLClassLoader) {
2490+
if (Set.of(((URLClassLoader) plugin.classLoader).getURLs()).contains(jarUrl)) {
2491+
return true;
24882492
}
24892493
}
2490-
return false;
24912494
}
2495+
return false;
24922496
}
24932497

24942498
@SuppressFBWarnings(value = "MS_SHOULD_BE_FINAL", justification = "for script console")
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
package hudson.util;
2+
3+
import java.util.Optional;
4+
import java.util.concurrent.ConcurrentHashMap;
5+
6+
/**
7+
/**
8+
* ClassLoader with internal caching of class loading results.
9+
*
10+
* <p>
11+
* Caches both successful and failed class lookups to avoid redundant delegation
12+
* and repeated class resolution attempts. Designed for performance optimization
13+
* in systems that repeatedly query class presence (e.g., plugin environments,
14+
* reflective loading, optional dependencies).
15+
* </p>
16+
*
17+
* Useful for classloaders that have heavy-weight loadClass() implementations
18+
*
19+
* @author Dmytro Ukhlov
20+
*/
21+
public class CachingClassLoader extends DelegatingClassLoader {
22+
private final ConcurrentHashMap<String, Object> loaded = new ConcurrentHashMap<>();
23+
24+
public CachingClassLoader(String name, ClassLoader parent) {
25+
super(name, parent);
26+
}
27+
28+
@Override
29+
protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
30+
Object classOrEmpty = loaded.computeIfAbsent(name, key -> {
31+
try {
32+
return doLoadClass(name);
33+
} catch (ClassNotFoundException e) {
34+
// Not found.
35+
return Optional.empty();
36+
}
37+
});
38+
39+
if (classOrEmpty == Optional.empty()) {
40+
throw new ClassNotFoundException(name);
41+
}
42+
43+
Class<?> clazz = (Class<?>) classOrEmpty;
44+
if (resolve) {
45+
resolveClass(clazz);
46+
}
47+
return clazz;
48+
}
49+
50+
/**
51+
* Do effective class loading
52+
*
53+
* @param name class name to load
54+
*
55+
* @return loaded class
56+
*
57+
* @throws ClassNotFoundException if class not found
58+
*/
59+
protected Class<?> doLoadClass(String name) throws ClassNotFoundException {
60+
return super.loadClass(name, false);
61+
}
62+
63+
public void clearCacheMisses() {
64+
loaded.values().removeIf(v -> v == Optional.empty());
65+
}
66+
}
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
package hudson.util;
2+
3+
/**
4+
* A {@link ClassLoader} that does not define any classes itself but delegates class loading
5+
* to other class loaders. It first attempts to load classes via its {@code getParent()} class loader,
6+
* then falls back to {@code findClass} to allow for custom delegation logic.
7+
* <p>
8+
* This class can also serve as the parent class loader for other class loaders that follow
9+
* the standard delegation model.
10+
*
11+
* @author Dmytro Ukhlov
12+
*/
13+
public class DelegatingClassLoader extends ClassLoader {
14+
protected DelegatingClassLoader(String name, ClassLoader parent) {
15+
super(name, parent);
16+
}
17+
18+
/**
19+
* Overrides base implementation to skip unnecessary synchronization
20+
*
21+
* @param name
22+
* The <a href="#binary-name">binary name</a> of the class
23+
*
24+
* @param resolve
25+
* If {@code true} then resolve the class
26+
*
27+
* @return The resulting {@code Class} object
28+
*
29+
* @throws ClassNotFoundException
30+
* If the class could not be found
31+
*/
32+
@Override
33+
protected Class<?> loadClass(String name, boolean resolve)
34+
throws ClassNotFoundException
35+
{
36+
Class<?> c = null;
37+
try {
38+
if (getParent() != null) {
39+
c = getParent().loadClass(name);
40+
}
41+
} catch (ClassNotFoundException e) {
42+
// ClassNotFoundException thrown if class not found
43+
// from the non-null parent class loader
44+
}
45+
46+
if (c == null) {
47+
// If still not found, then invoke findClass in order
48+
// to find the class.
49+
c = findClass(name);
50+
}
51+
52+
if (resolve) {
53+
resolveClass(c);
54+
}
55+
return c;
56+
}
57+
}

core/src/main/java/hudson/util/MaskingClassLoader.java

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -42,18 +42,14 @@
4242
*
4343
* @author Kohsuke Kawaguchi
4444
*/
45-
public class MaskingClassLoader extends ClassLoader {
45+
public class MaskingClassLoader extends DelegatingClassLoader {
4646
/**
4747
* Prefix of the packages that should be hidden.
4848
*/
4949
private final List<String> masksClasses;
5050

5151
private final List<String> masksResources;
5252

53-
static {
54-
registerAsParallelCapable();
55-
}
56-
5753
public MaskingClassLoader(ClassLoader parent, String... masks) {
5854
this(parent, Arrays.asList(masks));
5955
}

core/src/main/java/jenkins/security/s2m/JarURLValidatorImpl.java

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -83,12 +83,10 @@ public void validate(URL url) throws IOException {
8383
value = "DMI_COLLECTION_OF_URLS",
8484
justification = "All URLs point to local files, so no DNS lookup.")
8585
private static boolean isAllowedJar(URL url) {
86-
final ClassLoader classLoader = Jenkins.get().getPluginManager().uberClassLoader;
87-
if (classLoader instanceof PluginManager.UberClassLoader uberClassLoader) {
88-
if (uberClassLoader.isPluginJar(url)) {
89-
LOGGER.log(Level.FINER, () -> "Determined to be plugin jar: " + url);
90-
return true;
91-
}
86+
final PluginManager pluginManager = Jenkins.get().getPluginManager();
87+
if (pluginManager.isPluginJar(url)) {
88+
LOGGER.log(Level.FINER, () -> "Determined to be plugin jar: " + url);
89+
return true;
9290
}
9391

9492
final ClassLoader coreClassLoader = Jenkins.class.getClassLoader();

0 commit comments

Comments
 (0)