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?