The Garbage-First collector accepts a pause-time goal via -XX:MaxGCPauseMillis, but the goal is explicitly soft. Explain the machinery behind it and how you would reason about a service whose latency requirement is stricter than what that goal delivers.
answer
- goal is an input to sizing, not a guarantee
- decaying averages: copy cost, card scan cost
- young size + CSet size are the two dials
- lower goal -> smaller eden -> more promotion
- floor: young evacuated as a unit, full GC ignores goal
basics
~20 sG1 keeps statistics on past pauses and predicts the cost of evacuating candidate regions, then sizes the young generation and the collection set so the predicted pause fits the goal. It is a prediction, not a guarantee: mispredictions, humongous allocation, evacuation failure, and full GCs all overshoot it.
solid answer
~60 sG1 records the observed cost of past work - copy cost per byte, remembered-set scan cost per card, root scan time - and feeds a decaying-average prediction model. Before a pause it chooses how large young may grow and which old regions enter the collection set so that the *predicted* pause fits `-XX:MaxGCPauseMillis` (200 ms by default). It is soft for structural reasons. Predictions are averages over changing behaviour. Young regions cannot be partially collected, so there is a floor. Coarsened remembered sets, humongous allocations, and reference-processing spikes cost more than predicted. And when free regions run out, evacuation fails or a full compacting GC runs, and neither respects the goal. The trap is lowering the goal to chase a tail latency: a smaller goal means smaller young generations, more frequent pauses, more promotion of objects that would have died young, and eventually less reclamation per cycle. The honest reasoning is: give the collector headroom (heap and concurrent threads) first, examine whether the requirement is on mean or on p99.9, and if the tail requirement is genuinely tighter than G1's floor, that is a collector-selection decision, not a tuning one.
code
text · 8 lines-Xlog:gc*,gc+phases=debug,safepoint:file=gc.log:time,uptime,level,tags
# Look for, in order:
# 1. 'to-space exhausted' / 'Evacuation Failure' -> headroom problem, not a goal problem
# 2. 'Pause Full' -> cycle started too late or heap too small
# 3. Update RS / Scan RS dominating pause phases -> remembered-set pressure
# 4. Humongous Allocation as a pause cause -> region size or allocation shape
# Only if none of these dominate is MaxGCPauseMillis the relevant dial.go deeper
Know that the flag expresses a target, not a promise, and that the JVM adjusts how much it collects per pause to try to meet it.
Explain the two dials the goal drives - young size and collection-set size - and give at least one reason the target can be missed.
Diagnose from logs: distinguish a goal problem from a headroom, humongous, or remembered-set problem, and explain why lowering the goal often backfires through increased promotion.
Reason from the latency budget down: decide what fraction of the tail GC may consume, exhaust headroom and concurrency, and articulate the point where the stop-the-world evacuation floor makes this a collector-selection trade rather than a tuning exercise.
## What the goal actually controls `-XX:MaxGCPauseMillis` (default 200) is an input to G1's decision-making, not a limit enforced on the outcome. Two decisions consume it: 1. **How large the young generation may grow.** More eden means fewer, longer pauses; less eden means more frequent, shorter ones. G1 adjusts young size between pauses within the bounds of `-XX:G1NewSizePercent` and `-XX:G1MaxNewSizePercent`. 2. **How many old regions enter a mixed collection's collection set.** Candidates are added richest-in-garbage first while the predicted total still fits. ## The prediction model G1 maintains decaying averages of measured costs: bytes copied per unit time, cards scanned per unit time, per-region fixed overheads, root scan duration, and the time each recent pause actually took. From these it estimates the cost of evacuating any candidate region and sums estimates until the budget is used. Because the model is fed by recent history, it adapts to phase changes - but it always lags them. ## Why the goal is missed - **Model lag.** A workload that abruptly changes survival rate or reference density invalidates the averages; several pauses overshoot before the model catches up. - **An irreducible floor.** The young set is evacuated as a unit, and root scanning plus fixed pause overheads cannot be shrunk below some value. Asking for 5 ms on a large heap simply cannot be honoured. - **Remembered-set surprises.** Coarsened remembered sets force whole-region scans that the estimate under-counts. - **Reference processing and class unloading** can spike remark time independently of the collection set. - **Evacuation failure (to-space exhaustion).** If no free regions remain to copy into, G1 must self-heal in-pause, which is dramatically more expensive than a normal evacuation. - **Full GC.** The fallback is a whole-heap compacting collection. It ignores the goal entirely; it is parallel from JDK 10 onward, but still proportional to heap size. ## Reasoning about a stricter requirement Start by making the requirement precise. "Sub-50 ms" usually means a percentile of *end-to-end* request latency, not of GC pauses. GC contributes to that tail but so do safepoint bias, page faults, and scheduling. Establish from GC logs what the pause distribution actually is and what fraction of the latency budget it consumes at the percentile that matters. Then resist the reflex to lower the goal. Lowering it shrinks eden, which raises pause frequency, increases the total safepoint tax, and - because objects get less time to die in eden - promotes more short-lived data into the old generation, which increases the mixed-collection work that caused the pressure in the first place. Total throughput drops and the tail often gets worse, not better. The levers that genuinely help, in order: 1. **Headroom.** A larger heap gives more free regions to evacuate into and delays the initiating threshold crisis. Most G1 pause problems are actually headroom problems. 2. **Concurrency.** More concurrent marking and refinement threads let the cycle finish before the heap fills. 3. **Start earlier.** Beginning the marking cycle sooner buys time for mixed collections to keep up. 4. **Allocation behaviour.** Reducing humongous allocations (or raising region size so large arrays stop being humongous) and reducing cross-region reference churn attack the underlying cost. ## When it is a collector decision, not a tuning one G1's design accepts stop-the-world evacuation and therefore a pause that scales with the live data in the collection set. If a service's tail requirement is genuinely below what that floor permits on the heap size it needs, the answer is a collector whose compaction is concurrent rather than a smaller number in `MaxGCPauseMillis`. State that as a trade: those collectors buy sub-millisecond pauses with extra barrier cost and throughput, and with higher footprint. The principal-level answer names the trade and the measurement that decides it, rather than asserting a winner.
- A team sets -XX:MaxGCPauseMillis=20 on an 8 GB heap and reports that throughput collapsed and pauses did not reach 20 ms. Explain.G1 responds by shrinking the young generation so each evacuation is small, which multiplies pause frequency and the fixed per-pause overhead, so total GC CPU rises and application throughput falls. Because objects spend less time in eden, more of them survive to be promoted, increasing old-generation pressure and mixed-collection work. Meanwhile the fixed costs of root scanning and evacuating the young set as a unit keep actual pauses above the target, so nothing was gained.
- How would you decide between tuning G1 and moving to a concurrently compacting collector?Quantify what fraction of the tail latency budget GC pauses actually consume at the percentile the service is judged on, and check whether the dominant cost is evacuation work or a fixable condition such as evacuation failure or humongous allocation. If, after providing headroom and concurrency, the residual pause floor still exceeds the budget on the heap size the workload needs, that is structural to stop-the-world evacuation and is a collector choice - accepting extra barrier overhead and footprint in exchange for pauses that no longer scale with live data.
It is a delivery-time estimate from a courier, not a contract. The estimate is built from how long past deliveries took; unusual traffic or an oversized parcel breaks it, and demanding a shorter estimate does not make the van faster - it just means fewer parcels per trip and more trips.
saying these in an interview costs you the question
- Calling -XX:MaxGCPauseMillis a hard guarantee or a maximum the JVM enforces.
- Recommending a very low pause goal as the first response to latency complaints.
- Ignoring that a full GC or evacuation failure ignores the goal entirely.
- Assuming a smaller young generation is always better for latency, when it increases promotion and pause frequency.
- Tuning the goal before checking the GC log for evacuation failure, humongous allocation, or remembered-set-dominated pauses.