Skip to content

[JENKINS-75675] Refactor class loading logic in order to reduce memory consumption #16742

@jenkins-infra-bot

Description

@jenkins-infra-bot

I found that Jenkins plugin classloaders consume a significant, but unnecessary, amount of memory due to storing class loading locks.

The plugin classloader implementations (jenkins.util.URLClassLoader2 and hudson.PluginFirstClassLoader2) are based on the standard java.net.URLClassLoader, which extends the base class loading logic of java.lang.ClassLoader.

Here is a simplified pseudocode of java.lang.ClassLoader:

public abstract class ClassLoader {
  private final ClassLoader parent;

  private final ConcurrentHashMap<String, Object> parallelLockMap; 


  protected Object getClassLoadingLock(String className) {
    Object newLock = new Object();
    lock = parallelLockMap.putIfAbsent(className, newLock);
    if (lock == null) {
      lock = newLock;
    }
   
    return lock;
  }

  protected Class loadClass(String name, boolean resolve) {
    synchronized (getClassLoadingLock(name)) {
      Class c = findLoadedClass(name);
      if (c == null) {
if (parent != null) {
  c = parent.loadClass(name, false);
} else {
  c = findBootstrapClassOrNull(name);
}
      } 

      if (c == null) {
// If still not found, then invoke findClass in order
// to find the class.
c = findClass(name);
      }
      return c;
    }
}

As you can see, parallelLockMap retains entries for every class name ever attempted to be loaded, even if:

  • the class is located in a parent classloader,
  • the class name is invalid or nonexistent.

On my test Jenkins instance, I have about 200 plugins, and each of them uses around 5 MB for storing parallelLockMap, resulting in ~1 GB of wasted memory.

It appears that the standard JDK classloader implementations are well-suited for small, static class hierarchies, but not for dynamically evolving systems like Jenkins.

I suggest overriding the getClassLoadingLock() method in jenkins.util.URLClassLoader2 to use weak references for the lock objects. This would allow unused lock entries to be garbage-collected and reduce unnecessary memory retention.


Originally reported by dukhlov, imported from: Refactor class loading logic in order to reduce memory consumption
  • assignee: dukhlov
  • status: Closed
  • priority: Minor
  • component(s): core
  • label(s): 2.516.3-fixed
  • resolution: Fixed
  • resolved: 2025-08-16T15:14:49+00:00
  • votes: 1
  • watchers: 2
  • imported: 2025-11-24
Raw content of original issue

I found that Jenkins plugin classloaders consume a significant, but unnecessary, amount of memory due to storing class loading locks.

The plugin classloader implementations (jenkins.util.URLClassLoader2 and hudson.PluginFirstClassLoader2) are based on the standard java.net.URLClassLoader, which extends the base class loading logic of java.lang.ClassLoader.

Here is a simplified pseudocode of java.lang.ClassLoader:

public abstract class ClassLoader {
  private final ClassLoader parent;

  private final ConcurrentHashMap<String, Object> parallelLockMap; 


  protected Object getClassLoadingLock(String className) {
    Object newLock = new Object();
    lock = parallelLockMap.putIfAbsent(className, newLock);
    if (lock == null) {
      lock = newLock;
    }
   
    return lock;
  }

  protected Class<?> loadClass(String name, boolean resolve) {
    synchronized (getClassLoadingLock(name)) {
      Class<?> c = findLoadedClass(name);
      if (c == null) {
        if (parent != null) {
          c = parent.loadClass(name, false);
        } else {
          c = findBootstrapClassOrNull(name);
        }
      } 

      if (c == null) {
        // If still not found, then invoke findClass in order
        // to find the class.
        c = findClass(name);
      }
      return c;
    }
}

As you can see, parallelLockMap retains entries for every class name ever attempted to be loaded, even if:

  • the class is located in a parent classloader,
  • the class name is invalid or nonexistent.

On my test Jenkins instance, I have about 200 plugins, and each of them uses around 5 MB for storing parallelLockMap, resulting in ~1 GB of wasted memory.

It appears that the standard JDK classloader implementations are well-suited for small, static class hierarchies, but not for dynamically evolving systems like Jenkins.

I suggest overriding the getClassLoadingLock() method in jenkins.util.URLClassLoader2 to use weak references for the lock objects. This would allow unused lock entries to be garbage-collected and reduce unnecessary memory retention.

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions