Safepoints and time to safepoint: the JVM pauses your GC log does not show

Picture a latency spike several times longer than the GC pause logged at the same moment, and nobody able to explain the difference. Very often the missing time was spent before the collection started, waiting for every application thread to stop. That waiting period is called time to safepoint, and it is invisible unless you log it.
What a safepoint is
Some JVM operations need every Java thread to be stopped at a point where the VM knows exactly what is on its stack and in its registers: which slots hold object references, which frames are compiled, which locks are held. Such a point is a safepoint, and bringing all threads there is a global safepoint, the mechanism behind every "stop-the-world" phase.
The sequence is always the same. The VM thread decides it needs a safepoint for a VM operation, arms a poll that running threads check, waits until all of them have reached a safepoint or are in a state where they cannot touch the Java heap, runs the operation, then releases the threads. The total stop therefore has two parts:
- Time to safepoint (TTSP): from the request until the last thread has stopped.
- Time at safepoint: the operation itself, for example the evacuation phase of a G1 young collection.
The GC log reports the second part. Application threads experience both.
Which operations need one
Stop-the-world GC phases are the best-known case, but far from the only one. Depending on the JDK release and the collector, safepoint operations include:
- pauses of Serial, Parallel and G1, and the short pauses of ZGC and Shenandoah;
- printing a thread dump with
jstackorjcmd Thread.print, which is why a dump briefly stops the application (see reading thread dumps); - heap dumps and class histograms (
jcmd GC.heap_dump,GC.class_histogram); - class redefinition by agents and debuggers;
- some deoptimisation and code cache operations;
- on older JDKs, revocation of biased locks, a frequent source of short, unexplained pauses until biased locking was deprecated in JDK 15 by JEP 374 and later removed.
Since JDK 10, JEP 312 (thread-local handshakes) lets the VM run some operations on one thread at a time instead of stopping all of them, and later releases moved more work to handshakes or concurrent phases; ZGC, for instance, processes thread stacks concurrently since JDK 16 (JEP 376). Global safepoints have become rarer and shorter, but they have not disappeared.
How threads reach a safepoint
A thread's state decides how quickly it can stop:
- Running interpreted code: the interpreter checks for a pending safepoint between bytecodes, so it stops almost immediately.
- Running JIT-compiled code: the compilers insert polls at method returns and on loop back-edges. The thread stops at the next poll it executes.
- In native code or blocked: a thread inside a JNI call, waiting on a lock, sleeping or parked is already considered safe, because it cannot change Java objects. It does not delay the safepoint; it will block when it tries to return to Java code while the operation is still running.
TTSP is therefore set by the slowest thread that is running Java code at the moment of the request. One thread that takes a long time to reach its next poll holds up the whole JVM, including threads that stopped instantly.
Measuring it
Unified logging
The safepoint tag of unified logging prints one line per safepoint. Enable it next to your GC log, as described in reading GC logs:
-Xlog:gc*,safepoint:file=/var/log/app/gc.log:time,uptime,level,tags:filecount=10,filesize=50m
Each line names the VM operation and gives timings. On recent JDKs it looks roughly like this (abbreviated, with illustrative values; the exact fields vary between releases):
[info][safepoint] Safepoint "G1CollectForAllocation", Time since last: 2181040312 ns,
Reaching safepoint: 386201457 ns, At safepoint: 14920114 ns, Total: 401121571 ns
Here the collection itself took about 15 ms, and getting every thread to stop took about 386 ms. That is the signature of a TTSP problem: the fix lies in application threads, not in GC tuning.
Flight Recorder
JFR records safepoints as events, including jdk.SafepointBegin, jdk.SafepointStateSynchronization, jdk.SafepointEnd and jdk.ExecuteVMOperation. Check whether they are enabled, and with what threshold, in the settings file you record with; then look at them in JDK Mission Control alongside GC and thread events. See Flight Recorder and Mission Control for running recordings safely in production.
Finding the slow thread
Knowing that TTSP was long does not say which thread was late. Some JDK releases offer diagnostic options that report threads failing to reach a safepoint within a delay; their names and status (product or diagnostic) have changed over time, so check what your exact release supports with java -XX:+UnlockDiagnosticVMOptions -XX:+PrintFlagsFinal -version | grep -i safepoint before relying on them. In practice, correlating the spike with what the application was doing at that moment, from logs or a JFR recording, is often faster.
The usual causes of long TTSP
Long loops without a poll
To keep tight loops fast, the C2 compiler historically omitted the safepoint poll in counted loops, typically for (int i = 0; i < n; i++). A loop over a very large array then ran to completion before the thread could stop. Since JDK 10, C2 can apply loop strip mining: it splits such a loop into an outer loop with a poll and an inner loop of bounded length. It is controlled by -XX:+UseCountedLoopSafepoints and -XX:LoopStripMiningIter, and it is on by default with G1; whether it is on with other collectors depends on the release, so check the final flag values with -XX:+PrintFlagsFinal. Loops that call intrinsics, very large array copies or bulk operations can still run for a while without polling.
Page faults on memory-mapped files
A thread reading a MappedByteBuffer runs ordinary Java code. If the page it touches is not in memory, the operating system stalls the thread while it reads from disk, and during that time the thread is neither at a poll nor in a safe state. On slow or busy storage, a single page fault can stretch the pause for every thread by the latency of one disk read, or several. The same applies to any memory that has been swapped out, which is one more reason to keep a JVM away from swap.
CPU starvation
A thread can only reach its poll if it gets CPU time. When a host is overloaded, or a container hits its CPU quota and is throttled until the next period, runnable threads wait for the scheduler while the safepoint waits for them. TTSP spikes that line up with CPU throttling metrics point to sizing rather than code; see the JVM in containers for how the JVM sees CPU limits.
Very large thread counts
Bringing thousands of platform threads to a halt and releasing them has a cost of its own. It is rarely the dominant factor, but it shows up when an application runs huge thread pools on few cores.
A short checklist
- Log
gc*andsafepointtogether in production, with rotation. The overhead is small and the information is otherwise lost. - When a latency spike exceeds the GC pause, compare "Reaching safepoint" with "At safepoint" for that moment.
- If TTSP dominates, check CPU throttling and swap first, then storage latency for mapped files, then long-running loops in the code that was active.
- Remember that diagnostic commands also stop the world: a script that takes thread dumps every few seconds adds safepoints of its own.
Sources
- JEP 312: Thread-Local Handshakes, and JEP 376: ZGC Concurrent Thread-Stack Processing
- JEP 374: Deprecate and Disable Biased Locking
- The java command (unified logging with -Xlog) and the JDK Flight Recorder event reference
- OpenJDK bug JDK-8186027, C2: loop strip mining
Garbage collectionReading GC logs with -Xlog
One line of configuration, the legacy flag mapping, and how to read pause lines for leaks and undersized heaps.
Threads & CPUReading thread dumps
Take a series, read states and lock lines, and let the JVM point out deadlocks for you.
ProductionFlight Recorder and Mission Control
Continuous recording, on-demand dumps with jcmd, the events that matter, and reading .jfr files.