Repository: jenkinsci/jenkins
File: core/src/main/java/jenkins/util/URLClassLoader2.java (Java)

Code before:
     * @param urls the list of URLS to find classes in.
     * @param parent the parent to search for classes before we look in the {@code urls}
     * @since 2.459
     */
    public URLClassLoader2(String name, URL[] urls, ClassLoader parent) {
        super(name, urls, parent);
    }

    @Override
    public void addURL(URL url) {
        super.addURL(url);
    }

    @Override
    public Class<?> findClass(String name) throws ClassNotFoundException {
        return super.findClass(name);
    }

    @Override
    public Class<?> findLoadedClass2(String name) {
        return super.findLoadedClass(name);
    }

    @Override
    public Object getClassLoadingLock(String className) {
        return parallelLockWeakValuesMap.computeIfAbsent(className, k -> new Object());
    }
}


Review comment (performance):
I wonder why the JDK does not simply return an `intern`ed String and needs to keep the lock in a map.
For sure interning will put everything in the string pool, but if there are no references then the a String in the string pool should be eligible to be GC'ed  (normally a Sting literal in a class would cause a reference for as long as the class was loaded, meaning it would never actually be GCed, but for explicitly interned Strings this is different)

e.g

```java
  return className.intern();
```

There may be some valid reasons here - interning is not free and could cause a slowdown on classloading.
Or maybe this is just a hangover from older JDK versions before the string pool was moved to the heap.

Code after:
     * @param urls the list of URLS to find classes in.
     * @param parent the parent to search for classes before we look in the {@code urls}
     * @since 2.459
     */
    public URLClassLoader2(String name, URL[] urls, ClassLoader parent) {
        super(name, urls, parent);
    }

    @Override
    public void addURL(URL url) {
        super.addURL(url);
    }

    @Override
    public Class<?> findClass(String name) throws ClassNotFoundException {
        return super.findClass(name);
    }

    @Override
    public Class<?> findLoadedClass2(String name) {
        return super.findLoadedClass(name);
    }

    /**
     * Replace the JDK's per-name lock map with a GC-collectable lock object. This is a workaround
     * for JDK-8005233. When JDK-8005233 is resolved, this should be deleted. See also the
     * discussion in <a
     * href="https://mail.openjdk.org/pipermail/core-libs-dev/2025-May/146392.html">this OpenJDK
     * thread</a>.
     *
     * <p>Parallel-capable {@link ClassLoader} implementations keep a distinct lock object per class
     * name indefinitely, which can retain huge maps when there are many misses. Returning an
     * interned {@link String} keyed by this loader and the class name preserves mutual exclusion
     * for a given (loader, name) pair but allows the JVM to reclaim the lock when no longer
     * referenced. Interned Strings are heap objects and GC-eligible on modern JDKs (7+).
     *
     * @param className the binary name of the class being loaded (must not be null)
     * @return a lock object unique to this classloader/class pair
     */
    @Override
    public Object getClassLoadingLock(String className) {
        Objects.requireNonNull(className);
        return (lockObjectPrefixName + className).intern();
    }
}