-
-
Notifications
You must be signed in to change notification settings - Fork 9.4k
[JENKINS-75675] Refactor class loading logic in order to reduce memory consumption #10659
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
+186
−17
Merged
Changes from 11 commits
Commits
Show all changes
21 commits
Select commit
Hold shift + click to select a range
b0aa4a3
JENKINS-75675: Refactor classloader logic in order to reduce memory c…
bd316aa
Merge branch 'master' into feature/JENKINS-75675
basil d428e9b
grammar: Rename `ExistenceCheckingClassLoader` to `CheckingExistenceC…
basil c2a0994
Mark as restricted
basil 3aaac3c
Additional null checks
basil 6fd30eb
Mark as final
basil fab21ca
Add verification
basil 6363fbf
Clean up `ExistenceCheckingClassLoader`
basil 5986a69
Clean up `DelegatingClassLoader`
basil 20576ec
Improve documentation
basil f53da6e
Wrap lines at reasonable length
basil 02f5848
Make `DelegatingClassLoader` abstract
basil 6ad9f3b
Fast incremental
basil 2db6350
Revert "Fast incremental"
basil 4cf9156
Improve lockObject string calculation
344fffd
Add mail.openjdk.org link
fe65200
Merge branch 'master' into feature/JENKINS-75675
dukhlov a253e31
Add mail.openjdk.org link. Fix.
7bea4ca
Simplify method
basil 9b4e2c5
Clean up comment
basil 424b52f
Clean up formatting
basil File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,84 @@ | ||
| package hudson.util; | ||
|
|
||
| import java.util.Objects; | ||
| import org.kohsuke.accmod.Restricted; | ||
| import org.kohsuke.accmod.restrictions.NoExternalUse; | ||
|
|
||
| /** | ||
| * A {@link ClassLoader} that does not define any classes itself but delegates class loading to | ||
| * other class loaders to avoid the JDK's per-class-name locking and lock retention. | ||
| * | ||
| * <p>This class first attempts to load classes via its {@link ClassLoader#getParent} class loader, | ||
| * then falls back to {@link ClassLoader#findClass} to allow for custom delegation logic. | ||
| * | ||
| * <p>In a parallel-capable {@link ClassLoader}, the JDK maintains a per-name lock object | ||
| * indefinitely. In Jenkins, many class loading misses across many loaders can accumulate hundreds | ||
| * of thousands of such locks, retaining significant memory. This loader never defines classes and | ||
| * bypasses {@link ClassLoader}'s default {@code loadClass} locking; it delegates to the parent | ||
| * first and then to {@code findClass} for custom delegation. | ||
| * | ||
| * <p>The actual defining loader (parent or a delegate) still performs the necessary synchronization | ||
| * and class definition. A runtime guard ({@link #verify}) throws if this loader ever becomes the | ||
| * defining loader. | ||
| * | ||
| * <p>Subclasses must not call {@code defineClass}; implement delegation in {@code findClass} if | ||
| * needed and do not mark subclasses as parallel-capable. | ||
| * | ||
| * @author Dmytro Ukhlov | ||
| */ | ||
| @Restricted(NoExternalUse.class) | ||
| public class DelegatingClassLoader extends ClassLoader { | ||
| protected DelegatingClassLoader(String name, ClassLoader parent) { | ||
| super(name, Objects.requireNonNull(parent)); | ||
| } | ||
|
|
||
| public DelegatingClassLoader(ClassLoader parent) { | ||
| super(Objects.requireNonNull(parent)); | ||
| } | ||
|
|
||
| /** | ||
| * Parent-first delegation without synchronizing on {@link #getClassLoadingLock(String)}. This | ||
| * prevents creation/retention of per-name lock objects in a loader that does not define | ||
| * classes. The defining loader downstream still serializes class definition as required. | ||
| * | ||
| * @param name The binary name of the class | ||
| * @param resolve If {@code true} then resolve the class | ||
| * @return The resulting {@link Class} object | ||
| * @throws ClassNotFoundException If the class could not be found | ||
| */ | ||
| @Override | ||
| protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException { | ||
| Class<?> c = null; | ||
| try { | ||
| c = getParent().loadClass(name); | ||
| } catch (ClassNotFoundException e) { | ||
| // ClassNotFoundException thrown if class not found | ||
| // from the non-null parent class loader | ||
| } | ||
|
|
||
| if (c == null) { | ||
| // If still not found, then invoke findClass in order | ||
| // to find the class. | ||
| c = findClass(name); | ||
| } | ||
|
|
||
| c = verify(c); | ||
| if (resolve) { | ||
| resolveClass(c); | ||
| } | ||
| return c; | ||
| } | ||
|
|
||
| /** | ||
| * Safety check to ensure this delegating loader never becomes the defining loader. | ||
| * | ||
| * <p>Fails fast if a subclass erroneously defines a class here, which would violate the | ||
| * delegation-only contract and could reintroduce locking/retention issues. | ||
| */ | ||
| protected Class<?> verify(Class<?> clazz) { | ||
| if (clazz.getClassLoader() == this) { | ||
| throw new IllegalStateException("DelegatingClassLoader must not be the defining loader: " + clazz.getName()); | ||
| } | ||
| return clazz; | ||
| } | ||
| } |
60 changes: 60 additions & 0 deletions
60
core/src/main/java/hudson/util/ExistenceCheckingClassLoader.java
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,60 @@ | ||
| package hudson.util; | ||
|
|
||
| import java.util.Objects; | ||
| import jenkins.util.URLClassLoader2; | ||
| import org.kohsuke.accmod.Restricted; | ||
| import org.kohsuke.accmod.restrictions.NoExternalUse; | ||
|
|
||
| /** | ||
| * A {@link ClassLoader} that verifies the existence of a {@code .class} resource before attempting | ||
| * to load the class. Intended to sit in front of servlet container loaders we do not control. | ||
| * | ||
| * <p>This implementation overrides {@link #loadClass(String, boolean)} and uses {@link | ||
| * #getResource(String)} to check whether the corresponding <code>.class</code> file is available in | ||
| * the classpath. If the resource is not found, a {@link ClassNotFoundException} is thrown | ||
| * immediately. | ||
| * | ||
| * <p>Parallel-capable parent loaders retain a per-class-name lock object for every load attempt, | ||
| * including misses. By checking getResource(name + ".class") first and throwing {@link | ||
| * ClassNotFoundException} on absence, we avoid calling {@code loadClass} on misses, thus preventing | ||
| * the parent from populating its lock map for nonexistent classes. | ||
| * | ||
| * <p>This class is only needed in {@link hudson.PluginManager.UberClassLoader}. It is unnecessary | ||
| * for plugin {@link ClassLoader}s (because {@link URLClassLoader2} mitigates lock retention via | ||
| * {@link ClassLoader#getClassLoadingLock}) and redundant for delegators (because {@link | ||
| * DelegatingClassLoader} already avoids base locking). | ||
| * | ||
| * @author Dmytro Ukhlov | ||
| * @see ClassLoader | ||
| * @see #getResource(String) | ||
| */ | ||
| @Restricted(NoExternalUse.class) | ||
| public final class ExistenceCheckingClassLoader extends DelegatingClassLoader { | ||
|
|
||
| public ExistenceCheckingClassLoader(String name, ClassLoader parent) { | ||
| super(name, Objects.requireNonNull(parent)); | ||
| } | ||
|
|
||
| public ExistenceCheckingClassLoader(ClassLoader parent) { | ||
| super(Objects.requireNonNull(parent)); | ||
| } | ||
|
|
||
| /** | ||
| * Short-circuits misses by checking for the {@code .class} resource prior to delegation. | ||
| * Successful loads behave exactly as the parent would; misses do not touch the parent's | ||
| * per-name lock map. | ||
| */ | ||
| @Override | ||
| protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException { | ||
| // Add support for loading of JaCoCo dynamic instrumentation classes | ||
| if (name.equals("java.lang.$JaCoCo")) { | ||
| return verify(super.loadClass(name, resolve)); | ||
| } | ||
|
|
||
| if (getResource(name.replace('.', '/') + ".class") == null) { | ||
| throw new ClassNotFoundException(name); | ||
| } | ||
|
|
||
| return verify(super.loadClass(name, resolve)); | ||
| } | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I almost forgot: can you please update this comment to refer to the upstream JDK issue? Any deviation from upstream in
URLClassLoader2is a temporary workaround, but based on the discussion in this thread there is a sound justification. So please explain that this code should be removed once the upstream issue (with a link) is fixed.