DEV Community

Machine coding Master
Machine coding Master

Posted on

Stop Guessing Micro-Stalls: Debugging JIT Safepoint Latency with JDK 26 JFR

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.GarbageCollection JFR 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 waitThread field in jdk.SafepointWait events 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)
// }
Enter fullscreen mode Exit fullscreen mode

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:+UseCountedLoopSafepoints when 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)