diff --git a/independent-projects/bootstrap/runner/src/main/java/io/quarkus/bootstrap/runner/ClassLoadingResource.java b/independent-projects/bootstrap/runner/src/main/java/io/quarkus/bootstrap/runner/ClassLoadingResource.java index a6a2540601a2d..3dc5ca1fc6b03 100644 --- a/independent-projects/bootstrap/runner/src/main/java/io/quarkus/bootstrap/runner/ClassLoadingResource.java +++ b/independent-projects/bootstrap/runner/src/main/java/io/quarkus/bootstrap/runner/ClassLoadingResource.java @@ -36,4 +36,25 @@ public interface ClassLoadingResource { default void resetInternalCaches() { //no-op } + + /** + * Notifies this ClassLoadingResource that the definition of a class is about to begin. + * + * @param className The name of the class to be defined. + * @return true if the ClassLoader should actually attempt the definition of this class, false if the definition of the same + * class has been already requested by a different thread and then the current thread should wait for that + * definition to be completed and load the class without redefining it. + */ + default boolean definingClass(String className) { + return true; + } + + /** + * Notifies this ClassLoadingResource that the definition of a class is terminated. + * + * @param className The name of the class to be defined. + */ + default void classDefined(String className) { + //no-op + } } diff --git a/independent-projects/bootstrap/runner/src/main/java/io/quarkus/bootstrap/runner/JarResource.java b/independent-projects/bootstrap/runner/src/main/java/io/quarkus/bootstrap/runner/JarResource.java index 2f56ebfab104b..46c9f577a0a67 100644 --- a/independent-projects/bootstrap/runner/src/main/java/io/quarkus/bootstrap/runner/JarResource.java +++ b/independent-projects/bootstrap/runner/src/main/java/io/quarkus/bootstrap/runner/JarResource.java @@ -1,5 +1,7 @@ package io.quarkus.bootstrap.runner; +import static io.quarkus.bootstrap.runner.VirtualThreadSupport.isVirtualThread; + import java.io.IOException; import java.io.InputStream; import java.net.MalformedURLException; @@ -30,6 +32,9 @@ public class JarResource implements ClassLoadingResource { final Path jarPath; final AtomicReference> jarFileReference = new AtomicReference<>(); + // Single-entry cache of the name of the class currently loaded + private final AtomicReference loadingClass = new AtomicReference<>(); + public JarResource(ManifestInfo manifestInfo, Path jarPath) { this.manifestInfo = manifestInfo; this.jarPath = jarPath; @@ -56,6 +61,51 @@ public byte[] getResourceData(String resource) { return JarFileReference.withJarFile(this, resource, JarResourceDataProvider.INSTANCE); } + @Override + public boolean definingClass(String className) { + if (isVirtualThread()) { + // Use full non-blocking algorithm for virtual threads + return ClassLoadingResource.super.definingClass(className); + } + + if (loadingClass.compareAndSet(null, className)) { + // First thread trying to load this class, return true to signal that it has to be defined + return true; + } + + if (className.equals(loadingClass.get())) { + try { + synchronized (this) { + // Another thread already started the definition of this class, wait for its completion + this.wait(); + } + } catch (InterruptedException e) { + throw new RuntimeException(e); + } + return false; + } + + // The single value cache is already occupied by another class, use the full non-blocking algorithm + return true; + } + + @Override + public void classDefined(String className) { + if (isVirtualThread()) { + // Use full non-blocking algorithm for virtual threads + ClassLoadingResource.super.classDefined(className); + return; + } + + // The definition of the class has been completed, so make the single value cache available again ... + if (loadingClass.compareAndSet(className, null)) { + synchronized (this) { + // ... and notify other threads eventually waiting that they can now load the defined class + this.notifyAll(); + } + } + } + private static class JarResourceDataProvider implements JarFileReference.JarFileConsumer { private static final JarResourceDataProvider INSTANCE = new JarResourceDataProvider(); diff --git a/independent-projects/bootstrap/runner/src/main/java/io/quarkus/bootstrap/runner/RunnerClassLoader.java b/independent-projects/bootstrap/runner/src/main/java/io/quarkus/bootstrap/runner/RunnerClassLoader.java index 42599e6ab1131..62dccadb434c4 100644 --- a/independent-projects/bootstrap/runner/src/main/java/io/quarkus/bootstrap/runner/RunnerClassLoader.java +++ b/independent-projects/bootstrap/runner/src/main/java/io/quarkus/bootstrap/runner/RunnerClassLoader.java @@ -4,6 +4,7 @@ import static io.quarkus.commons.classloading.ClassLoaderHelper.isInJdkPackage; import java.net.URL; +import java.security.ProtectionDomain; import java.util.ArrayList; import java.util.Collections; import java.util.Enumeration; @@ -92,48 +93,47 @@ public Class loadClass(String name, boolean resolve) throws ClassNotFoundExce if (loaded != null) { return loaded; } - final ClassLoadingResource[] resources; - if (packageName == null) { - resources = resourceDirectoryMap.get(""); - } else { - String dirName = packageName.replace('.', '/'); - resources = resourceDirectoryMap.get(dirName); - } + + final ClassLoadingResource[] resources = findLoadingResources(packageName); if (resources != null) { String classResource = fromClassNameToResourceName(name); for (ClassLoadingResource resource : resources) { accessingResource(resource); byte[] data = resource.getResourceData(classResource); - if (data == null) { - continue; + if (data != null) { + return findOrDefineClass(resource, packageName, name, data); } - definePackage(packageName, resources); - return defineClass(name, data, resource); } } return getParent().loadClass(name); } - private void definePackage(String pkgName, ClassLoadingResource[] resources) { + private ClassLoadingResource[] findLoadingResources(String packageName) { + if (packageName == null) { + return resourceDirectoryMap.get(""); + } + String dirName = packageName.replace('.', '/'); + return resourceDirectoryMap.get(dirName); + } + + private void definePackage(String pkgName, ClassLoadingResource classPathElement) { if ((pkgName != null) && getDefinedPackage(pkgName) == null) { - for (ClassLoadingResource classPathElement : resources) { - ManifestInfo mf = classPathElement.getManifestInfo(); - if (mf != null) { - try { - definePackage(pkgName, mf.getSpecTitle(), - mf.getSpecVersion(), - mf.getSpecVendor(), - mf.getImplTitle(), - mf.getImplVersion(), - mf.getImplVendor(), null); - } catch (IllegalArgumentException e) { - var loaded = getDefinedPackage(pkgName); - if (loaded == null) { - throw e; - } + ManifestInfo mf = classPathElement.getManifestInfo(); + if (mf != null) { + try { + definePackage(pkgName, mf.getSpecTitle(), + mf.getSpecVersion(), + mf.getSpecVendor(), + mf.getImplTitle(), + mf.getImplVersion(), + mf.getImplVendor(), null); + } catch (IllegalArgumentException e) { + var loaded = getDefinedPackage(pkgName); + if (loaded == null) { + throw e; } - return; } + return; } try { definePackage(pkgName, null, null, null, null, null, null, null); @@ -146,12 +146,21 @@ private void definePackage(String pkgName, ClassLoadingResource[] resources) { } } - private Class defineClass(String name, byte[] data, ClassLoadingResource resource) { - Class loaded; + private Class findOrDefineClass(ClassLoadingResource resource, String packageName, String name, byte[] data) { + if (resource.definingClass(name)) { + definePackage(packageName, resource); + Class definedClass = defineClass(name, data, resource.getProtectionDomain()); + resource.classDefined(name); + return definedClass; + } + return findLoadedClass(name); + } + + private Class defineClass(String name, byte[] data, ProtectionDomain protectionDomain) { try { - return defineClass(name, data, 0, data.length, resource.getProtectionDomain()); + return defineClass(name, data, 0, data.length, protectionDomain); } catch (LinkageError e) { - loaded = findLoadedClass(name); + Class loaded = findLoadedClass(name); if (loaded != null) { return loaded; }