Greetings,
I saw class initialization will be preemptible in many cases in JDK 26, which 
is exciting. I believe my application is hitting a deadlock due to virtual 
threads pinned in class loading on OpenJDK 25.0.1.

I captured a stack dump of the deadlocked application, and I can share the 
interesting parts of the dump. For background, there are 32 cores, so the 
virtual thread scheduler pool has 32 carrier threads.

There are 30 virtual threads with stacks like this, loading a particular class:

#1048 "" virtual BLOCKED 2025-12-12T19:38:17.499565442Z
    at java.base/jdk.internal.loader.BuiltinClassLoader.loadClassOrNull(Unknown 
Source)
    - waiting to lock <java.lang.Object@110bf5c4>
    at java.base/jdk.internal.loader.BuiltinClassLoader.loadClass(Unknown 
Source)
    at java.base/java.lang.ClassLoader.loadClass(Unknown Source)

One virtual thread won the class loader lock race and got a little farther:

#766 "" virtual BLOCKED 2025-12-12T19:38:17.502444574Z
    at java.base/java.util.zip.ZipFile.getMetaInfVersions(Unknown Source)
    - waiting to lock <java.util.jar.JarFile@74e199a1>
    at java.base/java.util.zip.ZipFile$1.getMetaInfVersions(Unknown Source)
    at java.base/java.util.jar.JarFile.getVersionedEntry(Unknown Source)
    at java.base/java.util.jar.JarFile.getEntry(Unknown Source)
    at java.base/java.util.jar.JarFile.getJarEntry(Unknown Source)
    at java.base/jdk.internal.loader.URLClassPath$JarLoader.getResource(Unknown 
Source)
    at java.base/jdk.internal.loader.URLClassPath.getResource(Unknown Source)
    at 
java.base/jdk.internal.loader.BuiltinClassLoader.findClassOnClassPathOrNull(Unknown
 Source)
    at java.base/jdk.internal.loader.BuiltinClassLoader.loadClassOrNull(Unknown 
Source)
    - locked <java.lang.Object@110bf5c4>
    at java.base/jdk.internal.loader.BuiltinClassLoader.loadClass(Unknown 
Source)
    at java.base/java.lang.ClassLoader.loadClass(Unknown Source)

Finally, one virtual thread is trying to load a different class:

#830 "" virtual BLOCKED 2025-12-12T19:38:17.503196029Z
    at java.base/java.util.zip.ZipFile.getMetaInfVersions(Unknown Source)
    - waiting to lock <java.util.jar.JarFile@74e199a1>
    at java.base/java.util.zip.ZipFile$1.getMetaInfVersions(Unknown Source)
    at java.base/java.util.jar.JarFile.getVersionedEntry(Unknown Source)
    at java.base/java.util.jar.JarFile.getEntry(Unknown Source)
    at java.base/java.util.jar.JarFile.getJarEntry(Unknown Source)
    at java.base/jdk.internal.loader.URLClassPath$JarLoader.getResource(Unknown 
Source)
    at java.base/jdk.internal.loader.URLClassPath.getResource(Unknown Source)
    at 
java.base/jdk.internal.loader.BuiltinClassLoader.findClassOnClassPathOrNull(Unknown
 Source)
    at java.base/jdk.internal.loader.BuiltinClassLoader.loadClassOrNull(Unknown 
Source)
    - locked <java.lang.Object@3877d539>
    at java.base/jdk.internal.loader.BuiltinClassLoader.loadClass(Unknown 
Source)
    at java.base/java.lang.ClassLoader.loadClass(Unknown Source)

Since these 32 virtual threads all have implicit class loading stacks, I assume 
they are pinned and consuming all virtual thread scheduler carrier capacity.

So, what's holding onto java.util.jar.JarFile@74e199a1? It's held by a 
class-loading platform thread:

#159 "" BLOCKED 2025-12-12T19:38:17.491871038Z
    at java.base/jdk.internal.ref.CleanerImpl$CleanableList.insert(Unknown 
Source)
    - waiting to lock <jdk.internal.ref.CleanerImpl$CleanableList@257e78>
    at java.base/jdk.internal.ref.PhantomCleanable.<init>(Unknown Source)
    at 
java.base/jdk.internal.ref.CleanerImpl$PhantomCleanableRef.<init>(Unknown 
Source)
    at java.base/java.lang.ref.Cleaner.register(Unknown Source)
    at 
java.base/java.util.zip.ZipFile$ZipFileInflaterInputStream.<init>(Unknown 
Source)
    at 
java.base/java.util.zip.ZipFile$ZipFileInflaterInputStream.<init>(Unknown 
Source)
    at java.base/java.util.zip.ZipFile.getInputStream(Unknown Source)
    - locked <java.util.jar.JarFile@74e199a1>
    at java.base/java.util.jar.JarFile.getInputStream(Unknown Source)
    - locked <java.util.jar.JarFile@74e199a1>
    at 
java.base/jdk.internal.loader.URLClassPath$JarLoader$1.getInputStream(Unknown 
Source)
    at java.base/jdk.internal.loader.Resource.cachedInputStream(Unknown Source)
    - locked <jdk.internal.loader.URLClassPath$JarLoader$1@6b4c387f>
    at java.base/jdk.internal.loader.Resource.getByteBuffer(Unknown Source)
    at java.base/jdk.internal.loader.BuiltinClassLoader.defineClass(Unknown 
Source)
    at 
java.base/jdk.internal.loader.BuiltinClassLoader.findClassOnClassPathOrNull(Unknown
 Source)
    at java.base/jdk.internal.loader.BuiltinClassLoader.loadClassOrNull(Unknown 
Source)
    - locked <java.lang.Object@4c64fcc7>
    at java.base/jdk.internal.loader.BuiltinClassLoader.loadClass(Unknown 
Source)
    at java.base/java.lang.ClassLoader.loadClass(Unknown Source)

The final piece of the puzzle is 
jdk.internal.ref.CleanerImpl$CleanableList@257e78. No stack in the dump is 
recorded as having locked this object. There is however this virtual thread:

#926 "" virtual RUNNABLE 2025-12-12T19:38:17.504366600Z
    at java.base/jdk.internal.ref.CleanerImpl$CleanableList.insert(Unknown 
Source)
    at java.base/jdk.internal.ref.PhantomCleanable.<init>(Unknown Source)
    at java.base/java.io.FileCleanable.<init>(Unknown Source)
    at java.base/java.io.FileCleanable.register(Unknown Source)
    at java.base/java.io.FileInputStream.<init>(Unknown Source)
    (some app class that reads files)

I'm guessing that the CleanableList object monitor was released and thread #926 
was woken. But it can't run because the virtual thread scheduler is starved.

Does this analysis seem sound? Is there an existing bug for this issue?

Reply via email to