Skip to content

Commit 9d97d2c

Browse files
author
Dmytro Ukhlov
committed
JENKINS-75675: Refactor classloader logic in order to reduce memory consumption
Override URLClassloader2.getClassLoadingLock to use lock objects that can be GCed when not needed, Wrap WebAppClassLoader with CheckingExistenceClassLoader to avoid missing loadClass() calls not to crate unnecessary persistent lock object, Refactor delegating classloaders not to use locking: * DelegatingClassloader - non-locking base implementation and use it for classloaders which delegate functionality to other classloaders, * CachingClassloader - for caching class loading result, useful if loadClass operation is heavy, * CheckingExistenceClassLoader - check that class exists before loadClass() calls.
1 parent 7732f55 commit 9d97d2c

File tree

8 files changed

+222
-58
lines changed

8 files changed

+222
-58
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: 36 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -58,8 +58,11 @@
5858
import hudson.security.ACLContext;
5959
import hudson.security.Permission;
6060
import hudson.security.PermissionScope;
61+
import hudson.util.CachingClassLoader;
62+
import hudson.util.CheckingExistenceClassLoader;
6163
import hudson.util.CyclicGraphDetector;
6264
import hudson.util.CyclicGraphDetector.CycleDetectedException;
65+
import hudson.util.DelegatingClassLoader;
6366
import hudson.util.FormValidation;
6467
import hudson.util.PersistedList;
6568
import hudson.util.Retrier;
@@ -106,13 +109,11 @@
106109
import java.util.Locale;
107110
import java.util.Map;
108111
import java.util.Objects;
109-
import java.util.Optional;
110112
import java.util.ServiceLoader;
111113
import java.util.Set;
112114
import java.util.TreeMap;
113115
import java.util.UUID;
114116
import java.util.concurrent.ConcurrentHashMap;
115-
import java.util.concurrent.ConcurrentMap;
116117
import java.util.concurrent.CopyOnWriteArrayList;
117118
import java.util.concurrent.Future;
118119
import java.util.function.Supplier;
@@ -356,7 +357,19 @@ PluginManager doCreate(@NonNull Class<? extends PluginManager> klass,
356357
*/
357358
// implementation is minimal --- just enough to run XStream
358359
// and load plugin-contributed classes.
359-
public final ClassLoader uberClassLoader = new UberClassLoader(activePlugins);
360+
public final ClassLoader uberClassLoader = new CachingClassLoader("Caching UberClassLoader",
361+
new UberClassLoader(activePlugins)) {
362+
@Override
363+
protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
364+
// basic check before use cache-based loading
365+
if (name.startsWith("SimpleTemplateScript") || // cf. groovy.text.SimpleTemplateEngine
366+
name.startsWith("groovy.tmp.")) {
367+
throw new ClassNotFoundException("ignoring " + name);
368+
}
369+
370+
return super.loadClass(name, resolve);
371+
}
372+
};
360373

361374
/**
362375
* Once plugin is uploaded, this flag becomes true.
@@ -538,7 +551,7 @@ protected void reactOnCycle(PluginWrapper q, List<PluginWrapper> cycle) {
538551
for (PluginWrapper p : cgd.getSorted()) {
539552
if (p.isActive()) {
540553
activePlugins.add(p);
541-
((UberClassLoader) uberClassLoader).clearCacheMisses();
554+
((CachingClassLoader) uberClassLoader).clearCacheMisses();
542555
}
543556
}
544557
} catch (CycleDetectedException e) { // TODO this should be impossible, since we override reactOnCycle to not throw the exception
@@ -660,7 +673,6 @@ void considerDetachedPlugin(String shortName, String source) {
660673
} catch (IOException e) {
661674
failedPlugins.add(new FailedPlugin(arc.getName(), e));
662675
}
663-
664676
}
665677
}
666678

@@ -973,7 +985,7 @@ public void dynamicLoad(File arc, boolean removeExisting, @CheckForNull List<Plu
973985
plugins.add(p);
974986
if (p.isActive()) {
975987
activePlugins.add(p);
976-
((UberClassLoader) uberClassLoader).clearCacheMisses();
988+
((CachingClassLoader) uberClassLoader).clearCacheMisses();
977989
}
978990

979991
// TODO antimodular; perhaps should have a PluginListener to complement ExtensionListListener?
@@ -2396,43 +2408,29 @@ public <T> T of(String key, Class<T> type, Supplier<T> func) {
23962408
/**
23972409
* {@link ClassLoader} that can see all plugins.
23982410
*/
2399-
public static final class UberClassLoader extends ClassLoader {
2411+
public static final class UberClassLoader extends DelegatingClassLoader {
24002412
private final List<PluginWrapper> activePlugins;
24012413

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-
24092414
public UberClassLoader(List<PluginWrapper> activePlugins) {
2410-
super("UberClassLoader", PluginManager.class.getClassLoader());
2415+
super("UberClassLoader", new CheckingExistenceClassLoader(PluginManager.class.getClassLoader()));
24112416
this.activePlugins = activePlugins;
24122417
}
24132418

24142419
@Override
24152420
protected Class<?> findClass(String name) throws ClassNotFoundException {
2416-
if (name.startsWith("SimpleTemplateScript")) { // cf. groovy.text.SimpleTemplateEngine
2417-
throw new ClassNotFoundException("ignoring " + name);
2418-
}
2419-
return loaded.computeIfAbsent(name, this::computeValue).orElseThrow(() -> new ClassNotFoundException(name));
2420-
}
2421-
2422-
private Optional<Class<?>> computeValue(String name) {
24232421
for (PluginWrapper p : activePlugins) {
24242422
try {
24252423
if (FAST_LOOKUP) {
2426-
return Optional.of(ClassLoaderReflectionToolkit.loadClass(p.classLoader, name));
2424+
return ClassLoaderReflectionToolkit.loadClass(p.classLoader, name);
24272425
} else {
2428-
return Optional.of(p.classLoader.loadClass(name));
2426+
return p.classLoader.loadClass(name);
24292427
}
24302428
} catch (ClassNotFoundException e) {
24312429
// Not found. Try the next class loader.
24322430
}
24332431
}
2434-
// Not found in any of the class loaders. Delegate.
2435-
return Optional.empty();
2432+
2433+
throw new ClassNotFoundException(name);
24362434
}
24372435

24382436
@Override
@@ -2464,31 +2462,28 @@ protected Enumeration<URL> findResources(String name) throws IOException {
24642462
return Collections.enumeration(resources);
24652463
}
24662464

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

24712466
@Override
24722467
public String toString() {
24732468
// only for debugging purpose
24742469
return "classLoader " + getClass().getName();
24752470
}
2471+
}
24762472

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-
}
2473+
// TODO Remove this once we require post 2024-07 remoting minimum version and deleted ClassLoaderProxy#fetchJar(URL)
2474+
@SuppressFBWarnings(
2475+
value = "DMI_COLLECTION_OF_URLS",
2476+
justification = "All URLs point to local files, so no DNS lookup.")
2477+
@Restricted(NoExternalUse.class)
2478+
public boolean isPluginJar(URL jarUrl) {
2479+
for (PluginWrapper plugin : activePlugins) {
2480+
if (plugin.classLoader instanceof URLClassLoader) {
2481+
if (Set.of(((URLClassLoader) plugin.classLoader).getURLs()).contains(jarUrl)) {
2482+
return true;
24882483
}
24892484
}
2490-
return false;
24912485
}
2486+
return false;
24922487
}
24932488

24942489
@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: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
package hudson.util;
2+
3+
/**
4+
* A class loader that verifies the existence of a class resource before attempting to load the class.
5+
* <p>
6+
* This implementation overrides {@link #loadClass(String, boolean)} and uses {@link #getResource(String)}
7+
* to check whether the corresponding <code>.class</code> file is available in the classpath.
8+
* If the resource is not found, a {@link ClassNotFoundException} is thrown immediately.
9+
* </p>
10+
*
11+
* <p>This approach can be useful in environments where conditional class availability
12+
* must be verified without triggering side effects from the loading process.</p>
13+
*
14+
* @see ClassLoader
15+
* @see #getResource(String)
16+
*/
17+
public class CheckingExistenceClassLoader extends DelegatingClassLoader {
18+
19+
public CheckingExistenceClassLoader(String name, ClassLoader parent) {
20+
super(name, parent);
21+
}
22+
23+
public CheckingExistenceClassLoader(ClassLoader parent) {
24+
super(parent);
25+
}
26+
27+
@Override
28+
protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
29+
if (getResource(name.replace('.', '/') + ".class") == null &&
30+
!name.equals("java.lang.$JaCoCo")) { // Add support for loading of JaCoCo dynamic instrumentation classes
31+
throw new ClassNotFoundException(name);
32+
}
33+
34+
return super.loadClass(name, resolve);
35+
}
36+
}
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
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+
public DelegatingClassLoader(ClassLoader parent) {
19+
super(parent);
20+
}
21+
22+
/**
23+
* Overrides base implementation to skip unnecessary synchronization
24+
*
25+
* @param name
26+
* The <a href="#binary-name">binary name</a> of the class
27+
*
28+
* @param resolve
29+
* If {@code true} then resolve the class
30+
*
31+
* @return The resulting {@code Class} object
32+
*
33+
* @throws ClassNotFoundException
34+
* If the class could not be found
35+
*/
36+
@Override
37+
protected Class<?> loadClass(String name, boolean resolve)
38+
throws ClassNotFoundException
39+
{
40+
Class<?> c = null;
41+
try {
42+
if (getParent() != null) {
43+
c = getParent().loadClass(name);
44+
}
45+
} catch (ClassNotFoundException e) {
46+
// ClassNotFoundException thrown if class not found
47+
// from the non-null parent class loader
48+
}
49+
50+
if (c == null) {
51+
// If still not found, then invoke findClass in order
52+
// to find the class.
53+
c = findClass(name);
54+
}
55+
56+
if (resolve) {
57+
resolveClass(c);
58+
}
59+
return c;
60+
}
61+
}

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)