Disclaimer: This issue has been AI-investigated. I (human) can confirm that it is real and reproducible as described below.
Summary
We hit intermittent SIGSEGV crashes in the GC scavenger on IBM Semeru / OpenJ9 0.61.0 (JDK 25) in a production service that uses virtual threads. The crashing thread is always a GC worker, and the corrupted reference is always read off a virtual-thread carrier stack.
I have reduced this to a standalone reproducer with no third-party dependencies that segfaults within 25–100 seconds on the stock ibm-semeru-runtimes:open-25-jre-noble image. Two controls isolate the cause:
- the same program on Eclipse Temurin 25 (HotSpot) runs clean
- the same program on OpenJ9 with platform threads instead of virtual threads runs clean
So the trigger is virtual threads on OpenJ9, not the workload itself.
The signatures match two issues that were fixed years ago — #16351 (closed for 0.37) and #21390 (closed for 0.54) — so this looks like a regression or a fresh instance in the same machinery. 0.61.0 is the newest release, so there is no newer build for us to move to.
Environment
openjdk version "25.0.4.1" 2026-08-18 LTS
IBM Semeru Runtime Open Edition 25.0.4.10 (build 25.0.4.1+1-LTS)
Eclipse OpenJ9 VM 25.0.4.10 (build 25.0.4.1+1-openj9-0.61.0, JRE 25 Linux amd64-64-Bit Compressed References 20260818_340 (JIT enabled, AOT enabled)
OpenJ9 - a1daf225f5
OMR - ebd02d912
JCL - fd5c7608e81 based on jdk-25.0.4.1+1)
- Image:
ibm-semeru-runtimes:open-25-jre-noble (unmodified, no agents, no native libraries)
- Arch: amd64, compressed references
- GC: default (gencon / scavenger)
- Reproduced on Linux 6.8 (local container) and observed in production on Linux 6.12.94 (EKS)
- No JVMTI agent, no profiler, no ASGCT, no JNI code involved — neither in the reproducer (stock image, plain
java -jar) nor in the production service (the only -agentlib in our Helm chart is JDWP, disabled by default)
Reproducer
Repro.java is attached below. It has no dependencies. The ingredients were chosen from the production dump:
- virtual threads (carriers appear as
ForkJoinPool-1-worker-N)
- direct
ByteBuffer allocation, so jdk/internal/ref/PhantomCleanable.<init> becomes hot, gets JIT-compiled, and sits on the carrier stacks — this is the exact method named in the production dump's stack-slot diagnostics
- heavy young-gen churn so scavenges fire constantly while those stacks are deep
Thread.yield() / LockSupport.parkNanos so continuations mount and unmount mid-frame
- occasional exception stack walks from interpreted frames
# compile
docker run --rm -v "$PWD":/w -w /w \
ibm-semeru-runtimes:open-25-jdk-noble javac Repro.java
# run (see frequency note below)
docker run --rm --cpus=4 -m 3g -v "$PWD":/w -w /w \
ibm-semeru-runtimes:open-25-jre-noble \
java -Xmx1g -Xmns96m -Xmnx96m -XX:MaxDirectMemorySize=1g \
-Dthreads=2000 -Dseconds=300 Repro
The small, fixed nursery (-Xmns96m -Xmnx96m) just raises the scavenge rate to make the crash arrive sooner; it is not required. Production crashed with default heap settings.
Frequency. The crash is intermittent: 6 of 17 runs (~35%) crashed across 4- and 6-CPU containers with 180–300 s time limits. When it does crash it is fast — observed at 25 s, 40 s, 75 s and 102 s into the run. Expect to run it a handful of times.
Controls
| # |
JVM |
threads |
outcome |
| 1 |
Semeru/OpenJ9 25.0.4.1 (0.61.0) |
virtual |
SIGSEGV after ~40 s (11.7 M iterations) |
| 2 |
Semeru/OpenJ9 25.0.4.1 (0.61.0) |
virtual |
SIGSEGV after ~25 s (4.7 M iterations) |
| 3 |
Eclipse Temurin 25 (HotSpot) |
virtual |
clean, 84 M iterations, exited normally |
| 4 |
Semeru/OpenJ9 25.0.4.1 (0.61.0) |
platform (-Dvirtual=false) |
clean, 10 M iterations |
| 5 |
Semeru/OpenJ9 25.0.4.1 (0.61.0), 6 CPUs |
virtual |
SIGSEGV (4th attempt) |
| 6 |
Semeru/OpenJ9 25.0.4.1 (0.61.0), 6 CPUs |
virtual |
SIGSEGV (4th attempt) |
Run 4 uses the identical JVM and identical allocation workload and differs only in virtual vs platform threads. Runs 5 and 6 were made with the exact Repro.java attached below, and produced the identical backtrace (calculateObjectDetailsForCopy → copyForVariant → scavengeRememberedSetList, vmState=0x0002000f).
Crash output from the reproducer
Unhandled exception
Type=Segmentation error vmState=0x0002000f
J9Generic_Signal_Number=00000018 Signal_Number=0000000b Error_Value=00000000 Signal_Code=00000001
Handler1=000077187EA44A10 Handler2=000077187E62A900 InaccessibleAddress=000000000000001A
RIP=000077187C7B8732 RSP=000077186CD7A3F0
TRAPNO=000000000000000E CR2=000000000000001A
Module=/opt/java/openjdk/lib/default/libj9gc29.so
Module_base_address=000077187C600000
Target=2_90_20260818_340 (Linux 6.8.0-138-generic)
CPU=amd64 (20 logical CPUs)
----------- Stack Backtrace -----------
_ZN22GC_ObjectModelDelegate29calculateObjectDetailsForCopyEP18MM_EnvironmentBaseP18MM_ForwardedHeaderPmS4_S4_+0x32 (libj9gc29.so+0x1b8732)
_ZN12MM_Scavenger14copyForVariantILb0EEEP8J9ObjectP22MM_EnvironmentStandardP18MM_ForwardedHeader+0x54 (libj9gc29.so+0x191814)
_ZN12MM_Scavenger25scavengeRememberedSetListEP22MM_EnvironmentStandard+0x803 (libj9gc29.so+0x18d5c3)
_ZN12MM_Scavenger24workThreadGarbageCollectEP22MM_EnvironmentStandard+0x2ae (libj9gc29.so+0x18f6be)
_ZN21MM_ParallelDispatcher3runEP18MM_EnvironmentBaseP7MM_Taskm+0x6f (libj9gc29.so+0x139a5f)
_ZN12MM_Scavenger8scavengeEP18MM_EnvironmentBase+0xcd (libj9gc29.so+0x17967d)
_ZN12MM_Scavenger24mainThreadGarbageCollectEP18MM_EnvironmentBaseP22MM_AllocateDescriptionbb+0x1c7 (libj9gc29.so+0x1871d7)
_ZN12MM_Scavenger22internalGarbageCollectEP18MM_EnvironmentBaseP17MM_MemorySubSpaceP22MM_AllocateDescription+0xb21 (libj9gc29.so+0x188741)
---------------------------------------
InaccessibleAddress=0x1A — the scavenger is dereferencing a small integer as an object pointer.
The original production crash (same defect, root-scanning path)
The production instance crashed in the thread root-scanning path rather than the remembered-set path. Both dumps are from the same JVM, moments apart:
Unhandled exception
Type=Segmentation error vmState=0x00000000
Handler1=00007F974BA44A10 Handler2=00007F974B62A900 InaccessibleAddress=0000000000000000
Module=/opt/java/openjdk/lib/default/libj9vm29.so
Target=2_90_20260818_340 (Linux 6.12.94)
CPU=amd64 (8 logical CPUs)
----------- Stack Backtrace -----------
walkStackFrames+0xb03 (libj9vm29.so+0x8d713)
_ZN32VM_BytecodeInterpreterCompressed3runEP10J9VMThread+0xf3e8 (libj9vm29.so+0xd6ee8)
bytecodeLoopCompressed+0xdd (libj9vm29.so+0xc7a6d)
---------------------------------------
followed by stack-slot validator output naming the carrier thread and the JIT'd PhantomCleanable.<init> frame:
0000000001932900: Object neither in heap nor stack-allocated in thread ForkJoinPool-1-worker-8
0000000001932900: O-Slot=0000000002559F58
0000000001932900: O-Slot value=00007F96F40374D8
0000000001932900: PC=00007F96C739F3C1
0000000001932900: framesWalked=6
0000000001932900: literals=0000000000000010
0000000001932900: jitInfo=00007F968FF36B78
0000000001932900: method=00007F974C3E3F58 (jdk/internal/ref/PhantomCleanable.<init>(Ljava/lang/Object;Ljava/lang/ref/Cleaner;)V) (JIT)
0000000001932900: stack=00000000025533C8-000000000255A3F0
Note literals=0x10, which is not a valid J9ConstantPool pointer — the interpreter frame linkage is corrupt.
Then the scavenger crashed walking that same stack:
Unhandled exception
Type=Segmentation error vmState=0x0002000f
Handler1=00007F974BA44A60 Handler2=00007F974B62A900 InaccessibleAddress=0000000000000000
Module=/opt/java/openjdk/lib/default/libj9vm29.so
Target=2_90_20260818_340 (Linux 6.12.94)
----------- Stack Backtrace -----------
j9stackmap_StackBitsForPC+0x6d5 (libj9vm29.so+0x1b8e75)
j9cached_StackBitsForPC+0x78 (libj9vm29.so+0x1b7dd8)
walkBytecodeFrameSlots+0x2ff (libj9vm29.so+0x8c8ff)
walkStackFrames+0xbf0 (libj9vm29.so+0x8d800)
_ZN28GC_VMThreadStackSlotIterator9scanSlotsEP10J9VMThreadS1_PvPFvP8J9JavaVMPP8J9ObjectS2_P16J9StackWalkStatePKvEbb+0x39 (libj9gc29.so+0x4a429)
_ZN14MM_RootScanner13scanOneThreadEP18MM_EnvironmentBaseP10J9VMThreadPv+0x117 (libj9gc29.so+0x3eb97)
_ZN14MM_RootScanner11scanThreadsEP18MM_EnvironmentBase+0xc1 (libj9gc29.so+0x3d391)
_ZN14MM_RootScanner9scanRootsEP18MM_EnvironmentBase+0x3e (libj9gc29.so+0x4003e)
_ZN12MM_Scavenger24workThreadGarbageCollectEP22MM_EnvironmentStandard+0x37a (libj9gc29.so+0x18f78a))
_ZN21MM_ParallelDispatcher16workerEntryPointEP18MM_EnvironmentBase+0x19c (libj9gc29.so+0x139ccc)
---------------------------------------
Relation to previously fixed issues
| issue |
relation |
closed in |
| #21390 |
same backtrace as our production crash: j9stackmap_StackBitsForPC → walkBytecodeFrameSlots → continuation slot scanning, labels comp:gc/vt |
0.54 (Java 24) |
| #16351 |
same backtrace as the reproducer: MM_Scavenger::copyForVariant, vmState=0x0002000f, virtual threads under stress |
0.37 (Java 19) |
| #15251 |
the MM_StackSlotValidator "Object neither in heap nor stack-allocated" diagnostics seen above |
0.37 (Java 19) |
Possibly related open issue: #23514
#23514 reports a SIGSEGV at the same crash site — walkStackFrames during GC scanSlots under MM_Scavenger, InaccessibleAddress=0x0, with garbage where object pointers should be.
The trigger there looks different from ours: it was diagnosed in-thread as an ASGCT problem (@tajila, 2026-03-17: "This is not a JFR issue, the crash is due to ASGCT"), i.e. asynchronous stack walking from a profiler signal handler racing the GC. We have no agent and no ASGCT involvement at all, and we are on JDK 25 rather than 17.
But both end up with the GC stack walker reading an inconsistent frame, so it is worth considering whether there is a shared underlying fragility in walkBytecodeFrameSlots / the stackmap cache when a frame is observed in a transient state — reached via an async signal walk in #23514, and via continuation mount/unmount here.
Relevant practically: that issue has been stalled since March 2026 because it could not be reproduced — @hangshao0 asked "It will be nice if you have a test program that can reproduce this issue", and @tajila reported being unable to reproduce it. The reproducer attached here is exactly that missing handle on this code path, even if the two triggers turn out to be unrelated.
Secondary problem: the JVM hangs after the GPF instead of terminating
Worth splitting into its own issue, but it is what turned this from a crash into a long outage for us.
After the segfault, the JVM does not exit. It gets stuck here and stays there indefinitely — two crashed containers were left running and were still stuck in this state 9 hours later. In production the pods sat dead until an operator deleted them by hand, 15 and 29 minutes after the crash:
JVMDUMP032I JVM requested JIT dump using '/w/jitdump.20260913.200422.1.0004.dmp' in response to an event
JVMDUMP051I JIT dump occurred in 'ForkJoinPool-1-worker-1' thread 0x0000000000223100
JVMDUMP053I JIT dump is recompiling Repro.lambda$main$1(JI)Ljava/lang/Object;
Because the process never exits, the container is never restarted by Kubernetes: the restart counter stays at 0 and the pod keeps reporting Running while serving nothing. Crash-loop alerting cannot see it.
Disabling only the JIT dump agent is enough to make the process terminate promptly:
| flags |
trials |
crashed |
crash → exit |
dumps retained |
| default |
2 |
2 |
never |
all — hangs in the JIT dump agent |
-Xdump:jit:none |
2 |
1 |
6 s |
javacore + snap |
-Xdump:none |
1 |
1 |
6 s |
none |
So the JIT dump agent appears to deadlock or loop when the GPF happened on a virtual-thread carrier and it then tries to recompile the method running there.
What we are doing meanwhile
Using -Xdump:jit:none to bound the damage, and moving the affected code path off virtual threads. Happy to run additional diagnostics — the reproducer is quick, so trace options, debug builds or candidate patches are easy for us to test.
Attachments
Repro.java
Summary
We hit intermittent SIGSEGV crashes in the GC scavenger on IBM Semeru / OpenJ9 0.61.0 (JDK 25) in a production service that uses virtual threads. The crashing thread is always a GC worker, and the corrupted reference is always read off a virtual-thread carrier stack.
I have reduced this to a standalone reproducer with no third-party dependencies that segfaults within 25–100 seconds on the stock
ibm-semeru-runtimes:open-25-jre-nobleimage. Two controls isolate the cause:So the trigger is virtual threads on OpenJ9, not the workload itself.
The signatures match two issues that were fixed years ago — #16351 (closed for 0.37) and #21390 (closed for 0.54) — so this looks like a regression or a fresh instance in the same machinery. 0.61.0 is the newest release, so there is no newer build for us to move to.
Environment
ibm-semeru-runtimes:open-25-jre-noble(unmodified, no agents, no native libraries)java -jar) nor in the production service (the only-agentlibin our Helm chart is JDWP, disabled by default)Reproducer
Repro.javais attached below. It has no dependencies. The ingredients were chosen from the production dump:ForkJoinPool-1-worker-N)ByteBufferallocation, sojdk/internal/ref/PhantomCleanable.<init>becomes hot, gets JIT-compiled, and sits on the carrier stacks — this is the exact method named in the production dump's stack-slot diagnosticsThread.yield()/LockSupport.parkNanosso continuations mount and unmount mid-frameThe small, fixed nursery (
-Xmns96m -Xmnx96m) just raises the scavenge rate to make the crash arrive sooner; it is not required. Production crashed with default heap settings.Frequency. The crash is intermittent: 6 of 17 runs (~35%) crashed across 4- and 6-CPU containers with 180–300 s time limits. When it does crash it is fast — observed at 25 s, 40 s, 75 s and 102 s into the run. Expect to run it a handful of times.
Controls
-Dvirtual=false)Run 4 uses the identical JVM and identical allocation workload and differs only in virtual vs platform threads. Runs 5 and 6 were made with the exact
Repro.javaattached below, and produced the identical backtrace (calculateObjectDetailsForCopy→copyForVariant→scavengeRememberedSetList,vmState=0x0002000f).Crash output from the reproducer
InaccessibleAddress=0x1A— the scavenger is dereferencing a small integer as an object pointer.The original production crash (same defect, root-scanning path)
The production instance crashed in the thread root-scanning path rather than the remembered-set path. Both dumps are from the same JVM, moments apart:
followed by stack-slot validator output naming the carrier thread and the JIT'd
PhantomCleanable.<init>frame:Note
literals=0x10, which is not a validJ9ConstantPoolpointer — the interpreter frame linkage is corrupt.Then the scavenger crashed walking that same stack:
Relation to previously fixed issues
j9stackmap_StackBitsForPC→walkBytecodeFrameSlots→ continuation slot scanning, labelscomp:gc/vtMM_Scavenger::copyForVariant,vmState=0x0002000f, virtual threads under stressMM_StackSlotValidator"Object neither in heap nor stack-allocated" diagnostics seen abovePossibly related open issue: #23514
#23514 reports a SIGSEGV at the same crash site —
walkStackFramesduring GCscanSlotsunderMM_Scavenger,InaccessibleAddress=0x0, with garbage where object pointers should be.The trigger there looks different from ours: it was diagnosed in-thread as an ASGCT problem (@tajila, 2026-03-17: "This is not a JFR issue, the crash is due to ASGCT"), i.e. asynchronous stack walking from a profiler signal handler racing the GC. We have no agent and no ASGCT involvement at all, and we are on JDK 25 rather than 17.
But both end up with the GC stack walker reading an inconsistent frame, so it is worth considering whether there is a shared underlying fragility in
walkBytecodeFrameSlots/ the stackmap cache when a frame is observed in a transient state — reached via an async signal walk in #23514, and via continuation mount/unmount here.Relevant practically: that issue has been stalled since March 2026 because it could not be reproduced — @hangshao0 asked "It will be nice if you have a test program that can reproduce this issue", and @tajila reported being unable to reproduce it. The reproducer attached here is exactly that missing handle on this code path, even if the two triggers turn out to be unrelated.
Secondary problem: the JVM hangs after the GPF instead of terminating
Worth splitting into its own issue, but it is what turned this from a crash into a long outage for us.
After the segfault, the JVM does not exit. It gets stuck here and stays there indefinitely — two crashed containers were left running and were still stuck in this state 9 hours later. In production the pods sat dead until an operator deleted them by hand, 15 and 29 minutes after the crash:
Because the process never exits, the container is never restarted by Kubernetes: the restart counter stays at 0 and the pod keeps reporting
Runningwhile serving nothing. Crash-loop alerting cannot see it.Disabling only the JIT dump agent is enough to make the process terminate promptly:
-Xdump:jit:none-Xdump:noneSo the JIT dump agent appears to deadlock or loop when the GPF happened on a virtual-thread carrier and it then tries to recompile the method running there.
What we are doing meanwhile
Using
-Xdump:jit:noneto bound the damage, and moving the affected code path off virtual threads. Happy to run additional diagnostics — the reproducer is quick, so trace options, debug builds or candidate patches are easy for us to test.Attachments
Repro.java