Stop Guessing Micro-Stalls: Debugging JIT Safepoint Latency with JDK 26 JFR
If your high-throughput virtual thread service suffers from random p99.9 latency spikes while CPU and heap metrics look completely calm, you are likely hitting JIT safepoint starvation. In JDK 26, single uncounted loops can freeze tens of thousands of mounted virtual threads simultaneously, leaving legacy APMs completely blind.
Why Most Developers Get This Wrong
-
Blaming Garbage Collection blindly: Developers waste days tuning G1GC/ZGC parameters when
jdk.GarbageCollectionJFR events show sub-millisecond pauses during p99 spikes. - Assuming Virtual Threads isolate latency: Virtual threads unmount during I/O, but an uncounted JIT loop stalls the underlying carrier thread—blocking global safepoints for the entire JVM instance.
- Relying on aggregated APM metrics: Standard APMs aggregate latency over 10-second windows, completely obscuring 40ms stop-the-world VM safepoint stalls.
The Right Way
Pinpoint exact code paths causing safepoint delays by correlating JFR jdk.SafepointWait and jdk.ExecuteVMOperation events directly with JIT loop optimizations.
- Enable JFR safepoint profiling in production to catch carrier threads that fail to reach a safepoint prompt.
- Trace the
waitThreadfield injdk.SafepointWaitevents to pinpoint the exact OS/carrier thread holding the JVM hostage. - Instruct the C2 compiler to insert safepoint polls into long-running loops using JVM flags.
- Correlate micro-stalls with carrier thread stack traces captured during the stall window.
Show Me The Code
Here is how an uncounted loop delays JVM safepoints across thousands of virtual threads, and how to capture it:
// Culprit: Long-running uncounted primitive loop (C2 eliminates safepoint checks by default)
public double computeAggregates(double[] rawData) {
double sum = 0.0;
for (int i = 0; i < rawData.length; i++) { // Stalls global safepoint if array is large
sum += Math.sin(rawData[i]);
}
return sum;
}
// 1. Enable via JVM args in JDK 26:
// -XX:+UseCountedLoopSafepoints -XX:CompileCommand=option,computeAggregates,UseCountedLoopSafepoints
// 2. Query JFR Event output using jfr CLI:
// jdk.SafepointWait {
// startTime = 14:02:01.102
// duration = 42.8 ms
// safepointId = 1042
// waitThread = "ForkJoinPool-1-worker-4" (Carrier Thread)
// }
Key Takeaways
- Virtual threads do not protect you from C2 loop optimizations that eliminate safepoint polls.
- JDK 26 JFR events (
jdk.SafepointWait) explicitly identify which worker thread delayed the VM operation. - Use
-XX:+UseCountedLoopSafepointswhen processing large in-memory primitive data structures under strict SLA targets.
I built javalld.com while prepping for senior roles — complete LLD problems with execution traces, not just theory.
Top comments (0)