Skip to content
Preptima
hardConceptScenarioSeniorStaffLead

Your GC log shows a stop-the-world pause far longer than the collection work inside it. Where did the rest of the time go?

Most likely into reaching the safepoint. Every application thread has to arrive at a poll point before the operation can begin, and one late thread stalls all of them, so the pause is time-to-safepoint plus the actual work. Safepoint logging separates the two.

5 min readUpdated 2026-07-29Target archetype: Big Tech, Enterprise Captive, Product Startup
Practice answering out loud

What the interviewer is scoring

  • Does the candidate split the pause into time spent reaching the safepoint and time spent in the operation
  • Whether they know a thread executing native code does not have to be stopped, and can say why
  • That the next step is turning on safepoint logging rather than changing the collector or the heap size
  • Can they name a code shape that delays a poll, and the JVM mechanism that bounds it
  • Whether operations other than garbage collection are recognised as stopping the world

Answer

What a safepoint is, and who waits for whom

A safepoint is a state in which the JVM knows exactly what every application thread is doing: where its stack frames are, which registers hold object references, and therefore which references it must update if it moves an object. Operations that need that certainty — most garbage collection phases, thread dumps, heap dumps, deoptimisation, revoking certain locking optimisations — cannot proceed while any thread might be halfway through updating a reference.

So the JVM asks all threads to stop, and it can only ask at points the compiler has prepared for it. Compiled code contains explicit poll instructions: on method return, at loop back-edges, at call sites. When a safepoint is requested, each thread continues until it hits the next poll and then parks. The operation begins when the last thread has parked and ends when they are all released.

That "last thread" is the whole answer to your question. The pause your application experiences is the time for the slowest thread to reach a poll, plus the duration of the operation itself. Only the second part appears in the collector's own accounting, which is why a collection reported as short can coincide with a request latency spike an order of magnitude larger.

sequenceDiagram
  participant VM as VM thread
  participant A as Thread A in polled code
  participant B as Thread B in a counted loop
  VM->>A: request safepoint
  VM->>B: request safepoint
  A-->>VM: parked at the next poll
  Note over B: no poll until the loop finishes
  B-->>VM: parked late
  VM->>VM: run the operation and release

The interesting part of that sequence is the gap between A parking and B parking: thread A is already stopped and doing no work, so its idle time is part of your pause even though nothing in the collector was running yet.

Split the pause before you tune anything

The reflex when a pause looks long is to change collector or heap settings, and it is the wrong reflex until you know which half of the pause is large. The JVM's unified logging, available since JDK 9, will tell you: -Xlog:safepoint reports each safepoint operation with the time spent reaching the safepoint separately from the time spent at it. If the reaching time dominates, no amount of heap tuning helps, because the collector was not the bottleneck. If the operation time dominates, you have an ordinary collector-tuning problem and can go and read the GC log properly.

The corollary is that pauses are not only a garbage-collection phenomenon. A monitoring agent taking a thread dump every few seconds is requesting a global safepoint every few seconds, and so is anything triggering repeated heap inspection. Seeing safepoint operations that have nothing to do with collection in that log is a common and satisfying diagnosis.

Where a thread fails to arrive promptly

The classic cause is a loop the compiler has decided needs no poll. HotSpot has historically omitted safepoint polls from the back-edge of a counted loop — one whose trip count is a known int expression — on the grounds that the loop will finish soon. If the body is short and the trip count is in the billions, "soon" is a long time, and every other thread waits for it. Since JDK 10, HotSpot can strip-mine such loops, splitting them into an outer loop over chunks with a poll on the outer back-edge, which bounds the worst case rather than eliminating it; the behaviour is controlled by -XX:+UseCountedLoopSafepoints. If you are on an older release, or you have a genuinely enormous counted loop, breaking it into an outer loop over blocks yourself has the same effect and needs no flags.

The other causes are less about compilation and more about the machine. A thread that page-faults on memory the operating system has swapped out cannot reach its poll until the page returns, which is why swap on a JVM host converts a memory problem into a latency problem for every thread. A very large System.arraycopy or array-zeroing operation runs without an intervening poll. And a thread that has been descheduled by a busy host simply is not running, so it cannot reach anything.

One case that looks like a cause and is not: a thread executing native code through JNI. Native code cannot touch the JVM's object graph without going through an explicit interface, so such a thread is already considered safe for the duration of the call, is not asked to stop, and will block on its own when it tries to return into Java. A long native call therefore does not delay a safepoint. Getting this right is a good discriminator, because the intuition runs the other way.

Cheaper stopping, and what is left

The mechanism for requesting a stop has itself improved. JEP 312, delivered in JDK 10, introduced thread-local handshakes, which let the JVM stop individual threads and execute a callback in each rather than always requiring a global safepoint. That removed some operations from the global-pause list — the stack-scanning kind of work that used to need everyone stopped can be done thread by thread — and it is why stack traces of the safepoint machinery differ between older and newer releases. It does not change the arithmetic for genuine global operations: those still wait for the last thread.

What is left, therefore, is an operational discipline rather than a flag. Log safepoints in production, not only when investigating, because the cost is small and the alternative is a latency mystery with no evidence. Alert on time-to-safepoint separately from pause duration, since they have different causes and different fixes. Keep the JVM off swap entirely. Look for the code that does bounded arithmetic over enormous ranges without calling anything, because a loop with no call and no allocation is also a loop with nothing that naturally polls. And treat any agent that periodically dumps threads as a latency cost you have chosen, which is a decision worth making deliberately instead of inheriting from a default configuration.

A stop-the-world pause is time-to-safepoint plus the operation, and only the second half is in the GC log. Measure the split with safepoint logging before touching the collector, because a late thread is not a heap problem and no heap setting will fix it.

Likely follow-ups

  • Why does a thread blocked inside a native call not need to be interrupted for a safepoint to begin?
  • What does loop strip mining change about a counted loop, and what does it cost?
  • How would you identify which thread is arriving late, rather than only that one is?
  • Which JVM operations besides collection require a global safepoint?

Related questions

Further reading

jvmsafepointsgarbage-collectionlatencytroubleshooting