class loading deadlock
Benjamin Peterson
benjamin at locrian.net
Mon Dec 15 05:41:58 UTC 2025
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 at 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 at 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 at 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 at 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 at 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 at 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 at 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 at 74e199a1>
at java.base/java.util.jar.JarFile.getInputStream(Unknown Source)
- locked <java.util.jar.JarFile at 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 at 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 at 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 at 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?
More information about the loom-dev
mailing list