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.
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 releaseThe 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
- A service dies with OutOfMemoryError in production. Walk me through diagnosing it.hardAlso on jvm and garbage-collection5 min
- Your p99 latency jumped tenfold after a deploy while p50 is unchanged. Diagnose it out loud.hardAlso on latency and garbage-collection6 min
- How does the .NET garbage collector decide what to collect, and how would you make a hot path allocate less?hardAlso on garbage-collection6 min
- Users say the LLM feature in your product feels slow. How do you work out what to fix?mediumAlso on latency4 min
- How do you tell whether a value escapes to the heap, and how would you find the allocations that are costing you?hardAlso on garbage-collection6 min
- Your application cannot reach a service that should be up. Walk me through diagnosing it from the shell.mediumAlso on troubleshooting6 min
- Memory keeps climbing in a Go service under load. How do you find out why, and what can you actually tune?hardAlso on garbage-collection5 min
- A long-running Python service keeps climbing in memory and never comes back down. How do you work out where the memory went?hardAlso on garbage-collection5 min