How Classloader Isolation in Jenkins Works: JenkinsClassLoader and DelegatingClassLoader Explained

Jenkins achieves classloader isolation by using the JenkinsClassLoader interface to expose protected loading methods and DelegatingClassLoader to delegate lookups without defining classes itself, preventing version conflicts between plugins while avoiding memory overhead from JDK synchronization locks.

Jenkinsci/jenkins implements a sophisticated classloader isolation architecture that enables hundreds of plugins to execute concurrently without classpath collisions. This system relies on two complementary abstractions: the JenkinsClassLoader interface that exposes internal ClassLoader methods for safe access, and the DelegatingClassLoader abstract class that handles delegation without becoming the defining loader for any class.

The JenkinsClassLoader Interface

The JenkinsClassLoader interface in core/src/main/java/jenkins/util/JenkinsClassLoader.java exposes protected ClassLoader methods that are normally inaccessible to external utilities. This design eliminates the need for reflection when Jenkins core needs to perform advanced class-loading operations.

The interface declares methods such as findClass, findLoadedClass2, findResource, findResources, and getClassLoadingLock. These methods allow Jenkins utilities to inspect class-loading state and acquire synchronization locks directly. The URLClassLoader2 class implements this interface, providing concrete behavior for URL-based class loading while exposing these protected internals.

Throughout the core codebase, ClassLoaderReflectionToolkit uses these exposed methods to perform uniform class-loading operations across different loader implementations. This approach avoids reflective access warnings on Java 11+ and provides a single entry point for class-loading logic.

DelegatingClassLoader: Lock-Free Delegation

The DelegatingClassLoader abstract class in core/src/main/java/hudson/util/DelegatingClassLoader.java forms the backbone of Jenkins' memory-efficient isolation strategy. Unlike standard classloaders, this loader never defines classes itself and deliberately bypasses the JDK's per-class-name locking mechanism.

When loadClass is invoked, the implementation performs parent-first delegation. It first attempts to load the class from its parent loader (typically the Jenkins core or another plugin), and only if that fails does it invoke the subclass-specific findClass method. Crucially, DelegatingClassLoader never calls defineClass, ensuring it never becomes the "defining loader" for any class.

The verify method enforces this contract by throwing an IllegalStateException if a subclass mistakenly defines a class. By avoiding ClassLoader.loadClass-style synchronization, the loader sidesteps the per-class-name lock objects that the JDK retains for the lifetime of the classloader. This optimization prevents memory exhaustion when Jenkins loads hundreds of plugins, as standard classloaders would accumulate millions of lock objects.

The Plugin Isolation Architecture

Core to Plugin Class-Loader Chain

The isolation flow begins with the system classloader loading Jenkins core classes. Each plugin receives its own DependencyClassLoader, which is a concrete subclass of DelegatingClassLoader defined as an inner class within core/src/main/java/hudson/ClassicPluginStrategy.java.

DependencyClassLoader maintains references to the plugin's declared dependencies and traverses the dependency graph to locate classes and resources in other plugins. This structure creates a hierarchical loading chain where plugins can share common core classes while maintaining isolation for their specific implementations.

Parent-First Delegation with Plugin Fallback

When a plugin requests a class, DelegatingClassLoader.loadClass first delegates to its parent. If the parent cannot locate the class, the findClass implementation in DependencyClassLoader searches the plugin's own JAR files and its transitive dependencies. This pattern provides parent-first semantics for shared Jenkins APIs while allowing plugin-first resolution for plugin-specific classes.

The ClassLoaderReflectionToolkit facilitates this process by providing static helper methods like loadClass and _findResource that work uniformly across any loader implementing JenkinsClassLoader. These utilities handle the low-level class-loading operations while respecting the isolation boundaries.

Specialized Loader Subclasses

For specific use cases, Jenkins provides concrete subclasses that extend DelegatingClassLoader:

Both subclasses inherit the memory-efficient delegation mechanism while adding their specific functionality.

Practical Implementation Examples

The following examples demonstrate how to interact with Jenkins' classloader isolation programmatically.

Loading a class from a plugin using the reflection toolkit:

// Assumes 'plugin' is a PluginWrapper instance
Class<?> clazz = ClassLoaderReflectionToolkit.loadClass(
    plugin.getClassLoader(),
    "org.example.MyPluginClass"
);

Creating a custom delegating loader for temporary plugins:

class TempPluginClassLoader extends DelegatingClassLoader {
    private final URL[] urls;
    
    TempPluginClassLoader(ClassLoader parent, URL[] urls) {
        super(parent);
        this.urls = urls;
    }
    
    @Override
    protected Class<?> findClass(String name) throws ClassNotFoundException {
        for (URL u : urls) {
            try (URLClassLoader ucl = new URLClassLoader(new URL[]{u}, null)) {
                return ucl.loadClass(name);
            }
        }
        throw new ClassNotFoundException(name);
    }
}

// Usage
ClassLoader parent = Jenkins.getInstance().getPluginManager().uberClassLoader;
TempPluginClassLoader cp = new TempPluginClassLoader(
    parent,
    new URL[]{new File("my-plugin.jar").toURI().toURL()}
);
Class<?> myClass = cp.loadClass("com.my.PluginMain");

Summary

Jenkins achieves scalable classloader isolation through a carefully designed architecture:

  • JenkinsClassLoader exposes protected ClassLoader methods via core/src/main/java/jenkins/util/JenkinsClassLoader.java, enabling safe access to loading internals without reflection.
  • DelegatingClassLoader in core/src/main/java/hudson/util/DelegatingClassLoader.java delegates parent-first without defining classes, avoiding JDK memory locks.
  • DependencyClassLoader implements the concrete isolation logic for plugins within ClassicPluginStrategy, managing dependency graphs and JAR scanning.
  • ClassLoaderReflectionToolkit provides uniform access to class-loading operations across different loader implementations.
  • Specialized loaders like CachingClassLoader and MaskingClassLoader extend the delegation model for specific performance and security requirements.

Frequently Asked Questions

How does DelegatingClassLoader prevent memory leaks compared to standard ClassLoader implementations?

Standard Java ClassLoader implementations create a per-class-name lock object for every class loaded, which persists for the lifetime of the classloader. In a system with hundreds of plugins, this accumulates millions of lock objects, causing significant memory overhead. DelegatingClassLoader never calls defineClass, so it never becomes the defining loader and avoids creating these locks entirely. The verify method enforces this by throwing IllegalStateException if a subclass attempts to define a class directly.

What is the difference between JenkinsClassLoader and DelegatingClassLoader?

JenkinsClassLoader is an interface that exposes protected ClassLoader methods like findClass and getClassLoadingLock for use by Jenkins utilities, implemented by URLClassLoader2. DelegatingClassLoader is an abstract class that implements the delegation logic for class loading, specifically designed to delegate to parents without defining classes itself. While JenkinsClassLoader solves the API visibility problem, DelegatingClassLoader solves the memory and isolation problem.

How does Jenkins handle class loading when plugins depend on each other?

The DependencyClassLoader (an inner class of ClassicPluginStrategy in core/src/main/java/hudson/ClassicPluginStrategy.java) extends DelegatingClassLoader and manages plugin dependencies. When loading a class, it first delegates to the parent loader (Jenkins core), then falls back to its findClass implementation which scans the plugin's own JAR and the JARs of its declared transitive dependencies. This allows plugins to share common libraries while maintaining isolation from unrelated plugins.

Why does DelegatingClassLoader use parent-first delegation instead of parent-last?

Parent-first delegation ensures that plugins always use the Jenkins core versions of shared classes, preventing version skew and ClassCastException errors when objects pass between plugin and core code. This approach maintains API consistency across the system while the findClass fallback allows plugins to load their specific implementations when core classes are not available. The verify method guarantees that the delegating loader itself never defines the class, ensuring proper synchronization happens only in the defining loader.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →