Threads & CPU

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

Threads & CPU · · Published 2026-10-29

Close-up of a chrome pressure gauge with a needle on a white dial
Time to safepoint is a delay you can measure, once you know where to look. Photo: rawpixel, CC0.

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:

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:

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:

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

  1. Log gc* and safepoint together in production, with rotation. The overhead is small and the information is otherwise lost.
  2. When a latency spike exceeds the GC pause, compare "Reaching safepoint" with "At safepoint" for that moment.
  3. 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.
  4. Remember that diagnostic commands also stop the world: a script that takes thread dumps every few seconds adds safepoints of its own.

Sources