Skip to content

Commit 20576ec

Browse files
committed
Improve documentation
1 parent 5986a69 commit 20576ec

File tree

4 files changed

+71
-28
lines changed

4 files changed

+71
-28
lines changed

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

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2408,6 +2408,18 @@ public static final class UberClassLoader extends DelegatingClassLoader {
24082408
/** Cache of loaded, or known to be unloadable, classes. */
24092409
private final ConcurrentMap<String, Optional<Class<?>>> loaded = new ConcurrentHashMap<>();
24102410

2411+
/**
2412+
* The servlet container's {@link ClassLoader} (the parent of Jenkins core) is
2413+
* parallel-capable and maintains its own growing {@link Map} of {@link
2414+
* ClassLoader#getClassLoadingLock} objects per class name for every load attempt (including
2415+
* misses), and we cannot override this behavior. Wrap the servlet container {@link
2416+
* ClassLoader} in {@link ExistenceCheckingClassLoader} to avoid calling {@link
2417+
* ClassLoader#getParent}'s {@link ClassLoader#loadClass(String, boolean)} at all for misses
2418+
* by first checking if the resource exists. If the resource does not exist, we immediately
2419+
* throw {@link ClassNotFoundException}. As a result, the servlet container's {@link
2420+
* ClassLoader} is never asked to try and fail, and it never creates/retains lock objects
2421+
* for those misses.
2422+
*/
24112423
public UberClassLoader(List<PluginWrapper> activePlugins) {
24122424
super("UberClassLoader", new ExistenceCheckingClassLoader(PluginManager.class.getClassLoader()));
24132425
this.activePlugins = activePlugins;

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

Lines changed: 28 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,22 @@
55
import org.kohsuke.accmod.restrictions.NoExternalUse;
66

77
/**
8-
* A {@link ClassLoader} that does not define any classes itself but delegates class loading
9-
* to other class loaders. It first attempts to load classes via its {@code getParent()} class loader,
10-
* then falls back to {@code findClass} to allow for custom delegation logic.
8+
* A {@link ClassLoader} that does not define any classes itself but delegates class loading to other class loaders to
9+
* avoid the JDK's per-class-name locking and lock retention.
1110
* <p>
12-
* This class can also serve as the parent class loader for other class loaders that follow
13-
* the standard delegation model.
11+
* This class first attempts to load classes via its {@link ClassLoader#getParent} class loader, then falls back to
12+
* {@link ClassLoader#findClass} to allow for custom delegation logic.
13+
* <p>
14+
* In a parallel-capable {@link ClassLoader{, the JDK maintains a per-name lock object indefinitely. In Jenkins, many
15+
* class loading misses across many loaders can accumulate hundreds of thousands of such locks, retaining significant
16+
* memory. This loader never defines classes and bypasses {@link ClassLoader}'s default {@code loadClass} locking; it
17+
* delegates to the parent first and then to {@code findClass} for custom delegation.
18+
* <p>
19+
* The actual defining loader (parent or a delegate) still performs the necessary synchronization and class definition.
20+
* A runtime guard ({@link #verify}) throws if this loader ever becomes the defining loader.
21+
* <p>
22+
* Subclasses must not call {@code defineClass}; implement delegation in {@code findClass} if needed and do not mark
23+
* subclasses as parallel-capable.
1424
*
1525
* @author Dmytro Ukhlov
1626
*/
@@ -25,18 +35,14 @@ public DelegatingClassLoader(ClassLoader parent) {
2535
}
2636

2737
/**
28-
* Overrides base implementation to skip unnecessary synchronization
29-
*
30-
* @param name
31-
* The <a href="#binary-name">binary name</a> of the class
32-
*
33-
* @param resolve
34-
* If {@code true} then resolve the class
38+
* Parent-first delegation without synchronizing on {@link #getClassLoadingLock(String)}. This
39+
* prevents creation/retention of per-name lock objects in a loader that does not define
40+
* classes. The defining loader downstream still serializes class definition as required.
3541
*
36-
* @return The resulting {@code Class} object
37-
*
38-
* @throws ClassNotFoundException
39-
* If the class could not be found
42+
* @param name The binary name of the class
43+
* @param resolve If {@code true} then resolve the class
44+
* @return The resulting {@link Class} object
45+
* @throws ClassNotFoundException If the class could not be found
4046
*/
4147
@Override
4248
protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
@@ -61,6 +67,12 @@ protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundE
6167
return c;
6268
}
6369

70+
/**
71+
* Safety check to ensure this delegating loader never becomes the defining loader.
72+
*
73+
* <p>Fails fast if a subclass erroneously defines a class here, which would violate the
74+
* delegation-only contract and could reintroduce locking/retention issues.
75+
*/
6476
protected Class<?> verify(Class<?> clazz) {
6577
if (clazz.getClassLoader() == this) {
6678
throw new IllegalStateException("DelegatingClassLoader must not be the defining loader: " + clazz.getName());

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

Lines changed: 23 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,30 @@
11
package hudson.util;
22

33
import java.util.Objects;
4+
import jenkins.util.URLClassLoader2;
45
import org.kohsuke.accmod.Restricted;
56
import org.kohsuke.accmod.restrictions.NoExternalUse;
67

78
/**
8-
* A class loader that verifies the existence of a class resource before attempting to load the class.
9-
* <p>
10-
* This implementation overrides {@link #loadClass(String, boolean)} and uses {@link #getResource(String)}
11-
* to check whether the corresponding <code>.class</code> file is available in the classpath.
12-
* If the resource is not found, a {@link ClassNotFoundException} is thrown immediately.
13-
* </p>
9+
* A {@link ClassLoader} that verifies the existence of a {@code .class} resource before attempting
10+
* to load the class. Intended to sit in front of servlet container loaders we do not control.
1411
*
15-
* <p>This approach can be useful in environments where conditional class availability
16-
* must be verified without triggering side effects from the loading process.</p>
12+
* <p>This implementation overrides {@link #loadClass(String, boolean)} and uses {@link
13+
* #getResource(String)} to check whether the corresponding <code>.class</code> file is available in
14+
* the classpath. If the resource is not found, a {@link ClassNotFoundException} is thrown
15+
* immediately.
1716
*
17+
* <p>Parallel-capable parent loaders retain a per-class-name lock object for every load attempt,
18+
* including misses. By checking getResource(name + ".class") first and throwing {@link
19+
* ClassNotFoundException} on absence, we avoid calling {@code loadClass} on misses, thus preventing
20+
* the parent from populating its lock map for nonexistent classes.
21+
*
22+
* <p>This class is only needed in {@link hudson.PluginManager.UberClassLoader}. It is unnecessary
23+
* for plugin {@link ClassLoader}s (because {@link URLClassLoader2} mitigates lock retention via
24+
* {@link ClassLoader#getClassLoadingLock}) and redundant for delegators (because {@link
25+
* DelegatingClassLoader} already avoids base locking).
26+
*
27+
* @author Dmytro Ukhlov
1828
* @see ClassLoader
1929
* @see #getResource(String)
2030
*/
@@ -29,6 +39,11 @@ public ExistenceCheckingClassLoader(ClassLoader parent) {
2939
super(Objects.requireNonNull(parent));
3040
}
3141

42+
/**
43+
* Short-circuits misses by checking for the {@code .class} resource prior to delegation.
44+
* Successful loads behave exactly as the parent would; misses do not touch the parent's
45+
* per-name lock map.
46+
*/
3247
@Override
3348
protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
3449
// Add support for loading of JaCoCo dynamic instrumentation classes

core/src/main/java/jenkins/util/URLClassLoader2.java

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -71,12 +71,16 @@ public Class<?> findLoadedClass2(String name) {
7171
}
7272

7373
/**
74-
* Use String.intern() to create ClassLoader/className unique lock object which can be GCed
74+
* Replace the JDK's per-name lock map with a GC-collectable lock object.
7575
*
76-
* @param className
77-
* The name of the to-be-loaded class
76+
* <p>Parallel-capable {@link ClassLoader} implementations keep a distinct lock object per class
77+
* name indefinitely, which can retain huge maps when there are many misses. Returning an
78+
* interned {@link String} keyed by this loader and the class name preserves mutual exclusion
79+
* for a given (loader, name) pair but allows the JVM to reclaim the lock when no longer
80+
* referenced. Interned Strings are heap objects and GC-eligible on modern JDKs (7+).
7881
*
79-
* @return lock object, unique per classloader/classname combination
82+
* @param className the binary name of the class being loaded (must not be null)
83+
* @return a lock object unique to this classloader/class pair
8084
*/
8185
@Override
8286
public Object getClassLoadingLock(String className) {

0 commit comments

Comments
 (0)