Why architecture matters here

The architecture matters because a safepoint pause is a global stall: when the VM decides to reach a safepoint, every application thread stops, not just the one that triggered it. A single thread allocating and forcing a young collection freezes the entire process, including request handlers that were nowhere near out of memory. This is why safepoint behavior is a whole-service latency property, not a per-thread one, and why a pause shows up as a synchronized notch across every request in flight at that instant. If you care about tail latency, you care about how often you reach a safepoint and how long the rendezvous takes — those two numbers set the floor under your p99.

It matters because the pause has two independently-broken halves, and people conflate them. The time-to-safepoint is the herding cost — how long until the last thread parks — and it is governed by poll-site density and what threads are doing. The safepoint operation time is the work done while stopped — the GC phase, the deoptimization, the bias revocation. A GC log that reports a long 'stop-the-world' does not tell you which half was slow, yet the fixes are completely different: a slow operation points at the collector or the heap; a slow TTSP points at a straggler thread with no poll in its hot path.

It matters because the mechanism is why 'safe' points exist at all: the runtime can only interpret a thread's stack — decide which words are live object references — at locations where the compiler emitted an OopMap, a map of live pointers for that exact instruction. Emitting that map everywhere would bloat compiled code and constrain the optimizer, so the compiler emits it only at chosen poll sites: method entries and returns, and the back edges of loops. This is a deliberate space-for-latency trade: fewer poll sites mean smaller, faster code but a longer worst-case gap before a thread can stop.

Finally, it matters because safepoints are the substrate under far more than garbage collection. Biased-lock revocation, deoptimizing a speculatively-compiled method when its assumption breaks, patching a call site during class loading, hot-swapping code for a debugger or instrumentation agent, and taking an accurate stack sample for a profiler all ride the same protocol. That is a double-edged sword: it means a burst of biased-lock revocations or a profiler sampling too aggressively can flood your application with global pauses that look, in a naive log, exactly like GC — because structurally they are the same stop-the-world event.

Advertisement

The architecture: every piece explained

Top row: the request and the rendezvous. When any subsystem needs the world stopped, the VM requests STW by flipping a global poll flag — in HotSpot, by protecting a special memory page so that any thread reading it takes a trap. Each application thread polls that flag at the poll sites the compiler inserted: at method call boundaries and at loop back edges, a cheap load instruction that, in the fast path, does nothing. When the flag is set, the next poll a thread executes traps, and the thread parks itself, first recording its exact frame state via the OopMap so the runtime knows which slots are references. Once the last thread has parked, all parked — the safepoint is reached and the VM owns a consistent snapshot of the heap and all stacks.

Middle row: the different states a thread can be in, because not every thread needs to actively stop. A thread running Java polls cheaply and keeps going until it hits a poll site, then parks. A thread in JNI or blocked on a monitor or I/O is already at a safe state — it is not touching the heap in a way the runtime cannot describe — so it is counted as parked immediately and only needs to check the flag before it re-enters Java. The dangerous case is the long counted loop: a loop the JIT proved iterates a bounded integer count may have had its back-edge poll optimized away, so a thread grinding through millions of iterations does not poll and cannot stop — its late arrival is exactly the TTSP spike. Meanwhile the VM does the work: with everyone parked, it runs the GC phase, the deoptimization, or the revocation that motivated the safepoint.

Bottom-left: the metric that captures the herding cost. Time-to-safepoint is the interval from raising the flag to the last thread parking, and its value is set entirely by the slowest straggler — the tail, not the average. Nine threads can park in microseconds and the whole application still waits tens of milliseconds because the tenth is deep in a poll-free loop. This is why TTSP is a max, not a mean: one bad thread taxes everyone.

Bottom-right and ops: resumption and what to watch. When the operation finishes the VM clears the flag and the parked threads resume, re-checking any state that changed under them. The ops strip names the surface to monitor: the TTSP p99 (how bad the herding tail gets), the safepoint pause count (how often you stop the world at all), the causes broken down by GC versus biased-lock revocation versus deoptimization (so you fix the right subsystem), and the guaranteed-safepoint interval — a periodic timer that forces a safepoint even with nothing to do, so cleanup work and sampling still happen, and which can itself be a source of unexplained pauses.

Safepoint — a global agreement that every application thread has paused at a known stateGC, deoptimization, biased-lock revocation, and stack sampling all need the whole VM briefly frozenVM requests STWsets global poll flagThreads pollat loop backedges + callsEach thread parksrecords exact frame stateAll parkedsafepoint reachedRunning Javapolls cheaply, keeps goingIn JNI / blockedalready at safe stateLong counted loopmay not poll -> TTSP spikeVM does the workGC / deopt / revokeTime-to-safepoint (TTSP)the tail is the slowest stragglerResumeclear flag, threads run againOps — watch TTSP p99 + safepoint pause count + causes (GC vs revoke vs deopt) + guaranteed-safepoint intervalflaghit pollparkpollmeasuredrunthenobserveobserve
A safepoint is a global rendezvous: the VM raises a poll flag, and each application thread, at the next poll site it reaches (a method call boundary or loop backedge), parks itself at a point where its stack and registers are in a known, walkable state. Threads already blocked in JNI or on a monitor are effectively parked. Once every thread is parked the VM does its work (GC, deoptimization, biased-lock revocation, stack sampling) and then clears the flag so threads resume. The time from request to the last thread parking is the time-to-safepoint, and its tail is set by the slowest straggler.
Advertisement

End-to-end flow

Trace one young-generation garbage collection from the moment the allocator runs dry to the moment mutators resume, watching the safepoint protocol carry the whole event.

Trigger and request: an application thread tries to allocate and finds Eden full. It cannot collect on its own — that needs the world stopped — so it posts a safepoint request for a young GC and the VM thread flips the global poll page to the armed state. From this instant, the next poll any thread executes will trap. The requesting thread itself parks first. The clock on time-to-safepoint starts now.

Herding to the safepoint: the other application threads keep running until each reaches its next poll site. A thread in a normal method hits a call boundary within microseconds, reads the armed page, traps, records its OopMap, and parks. A thread blocked in a socket read or executing native code through JNI is already safe and is counted immediately; it will simply check the flag before returning to Java. But one worker is inside a numeric loop the JIT compiled without a back-edge poll because it proved the loop count bounded — it will not poll until the loop finishes. Everyone else is parked and waiting on this one thread. The gap until it finally exits the loop and parks is the time-to-safepoint, and here it is the dominant cost of the whole pause.

The operation, with the world consistent: the last thread parks, the VM now holds a globally consistent view — no mutator can touch a reference — and it runs the young collection. It walks the OopMaps of every thread's stack and the card table to find live roots, evacuates the survivors out of Eden into a survivor space or the old generation, and updates every reference to point at the moved objects. Because every thread is parked at a described point, the collector can safely rewrite pointers held in registers and stack slots; nothing is racing it. This is the 'operation' half of the pause, and its length depends on how much survived, not on the herding.

Resume and the tail: with Eden reclaimed and references fixed, the VM clears the poll page. Each parked thread wakes, notices the flag is down, reconciles any state that moved (its own references were updated in place), and continues exactly where it left off — the loop worker resumes its next iteration, the allocator gets its now-free Eden slot. To an outside observer every request in flight saw a single synchronized stall equal to TTSP plus the collection time. If the operators later see this pause was mostly TTSP, the fix is not a bigger heap — it is restoring a poll to that hot loop by breaking it into shorter chunks.