skip to content

HotSpot's Parallel collector runs with adaptive size policy enabled by default. What exactly is it adapting, in what order of priority, and when would you deliberately disable it with -XX:-UseAdaptiveSizePolicy?

level: seniorimportance: should knowfreq 32%

answer

  1. Resizes eden / survivors / old + tenuring threshold
  2. Priority: pause goal > throughput goal > footprint
  3. MaxGCPauseMillis is a target, not a guarantee
  4. GCTimeRatio=99 → aim for 1% GC overhead
  5. Off for benchmarks, survivor collapse, measured stable workloads

basics

~20 s

It continuously resizes eden, the survivor spaces and the old generation, and adjusts the tenuring threshold, using measured collection statistics. Goals in priority order: the pause-time goal, then the throughput goal, then minimum footprint. Disable it when you need stable, explicitly-set generation sizes for reproducible behaviour.

solid answer

~60 s

Adaptive size policy is the ergonomics loop inside the Parallel collector. After each collection it feeds the measured pause times, survival rates and allocation rates into a model and resizes the spaces to move toward its goals. **What it tunes:** eden size, survivor space size (and therefore the effective survivor ratio), old-generation size within `-Xms`/`-Xmx`, and the tenuring threshold that decides when an object is promoted. **Goal priority, highest first:** 1. **Pause-time goal** — `-XX:MaxGCPauseMillis`. If pauses exceed it, generations shrink. 2. **Throughput goal** — `-XX:GCTimeRatio` (default 99, meaning aim for no more than 1% of time in GC). If GC overhead is too high, generations grow. 3. **Footprint** — if both goals are met, shrink the heap. The order matters: setting an aggressive pause goal will happily cost you throughput, because pause wins. **When to turn it off:** when you want deterministic, reproducible sizing — benchmarks, capacity tests, or a service whose steady-state profile you have already measured. Also when you observe pathological behaviour such as survivor spaces being shrunk toward zero, which forces premature promotion. Note that with the policy on, an explicit `-XX:SurvivorRatio` will be overridden.

code

text · 9 lines
text
# adaptive (default)
-XX:+UseParallelGC -XX:MaxGCPauseMillis=200 -XX:GCTimeRatio=99

# pinned sizing for a benchmark
-XX:+UseParallelGC -XX:-UseAdaptiveSizePolicy \
  -Xms4g -Xmx4g -Xmn1g -XX:SurvivorRatio=8 -XX:MaxTenuringThreshold=8

# observe what ergonomics is deciding and the sizes it lands on
-Xlog:gc,gc+ergo*=debug,gc+heap=debug:file=gc.log

go deeper

for a junior

Know that the JVM resizes the generations itself while running, and that MaxGCPauseMillis is a goal rather than a guarantee.

for a middle

Name the knobs (eden, survivors, old, tenuring threshold) and the three goals in priority order, and explain the pause-goal-costs-throughput feedback.

for a senior

Show you would read gc+ergo logs before changing flags, and recognise survivor collapse and premature promotion as reasons to pin sizes.

for a principal

Position it as a policy decision: adaptive heuristics for heterogeneous services, pinned deterministic sizing for measured workloads and benchmark fleets, plus the operational cost of non-reproducible runs.

## What ergonomics is The JVM does not want you to hand-tune generation sizes. On startup it picks a collector, a heap size and generation proportions from the machine's CPU count and memory; then, while running, the Parallel collector keeps adjusting those proportions based on what it actually measures. That runtime feedback loop is the **adaptive size policy**, controlled by `-XX:+UseAdaptiveSizePolicy`, which is on by default for this collector. ## The inputs it measures After each collection the policy updates decaying averages of: - **Pause duration** for young collections and for full collections. - **Survival rate** — how much of eden survived, and how much of the survivor space survived again. - **Allocation rate** — how fast the application fills eden, and hence how often collections occur. - **Promotion volume** — how much data moved into the old generation. From these it estimates how a change in size would change the outcome: a larger eden means less frequent but longer young collections; larger survivor spaces mean objects can age longer before promotion, reducing pressure on the old generation; a larger old generation means less frequent full collections but a longer compaction when one happens. ## The knobs it turns - **Eden size** — the main lever on young collection frequency. - **Survivor space size**, which is really the survivor ratio, i.e. how eden and the two survivor spaces divide the young generation. - **Young/old split**, and the total heap between `-Xms` and `-Xmx`. - **Tenuring threshold** — how many young collections an object must survive before promotion, bounded by `-XX:MaxTenuringThreshold`. If survivor spaces overflow, the policy lowers the threshold, promoting objects sooner. ## The three goals, in strict priority 1. **Pause-time goal** — `-XX:MaxGCPauseMillis=N`. This is not a guarantee, it is a target. If measured pauses exceed it, the policy shrinks the generation responsible: less data per collection means shorter stops. Note the feedback trap — smaller generations means *more* collections, so satisfying an aggressive pause goal directly costs throughput. 2. **Throughput goal** — `-XX:GCTimeRatio=N`, targeting a GC overhead of `1/(1+N)`. The default 99 asks for 1%. If GC overhead exceeds the target and the pause goal is being met, the policy grows generations, trading longer but rarer collections for less total overhead. 3. **Footprint** — only when both goals are comfortably met does the policy shrink the heap to give memory back. The priority ordering is the single most important thing to know: a pause goal always outranks the throughput goal. Setting `-XX:MaxGCPauseMillis=50` on a throughput collector is a common self-inflicted wound — it can drive the heap small enough that the application spends far more total time collecting than it would with no pause goal at all. ## Interactions and surprises - **Explicit ratios get overridden.** With the adaptive policy on, `-XX:SurvivorRatio` and similar explicit sizing flags are recomputed by the policy. Candidates who set them and then see different values in the log are usually hitting exactly this. - **Survivor collapse.** A well-known pathology: under certain workloads the policy shrinks survivor spaces toward nothing. Every young collection then promotes almost everything, the old generation fills quickly, and full collection frequency spikes. The symptom in the log is a young collection whose "after" occupancy is near zero while old-generation occupancy climbs in steps of the same size. - **Oscillation.** Because the policy reacts to a changing workload, a service with distinct phases (warm-up, batch window, idle) can spend its time chasing a moving target, with generation sizes swinging. - **Non-reproducibility.** Two runs of the same benchmark on the same machine can size differently depending on timing, which makes measurement noisy. ## When to disable it Set `-XX:-UseAdaptiveSizePolicy` and pin sizes explicitly when: - You are **benchmarking or capacity testing** and need runs to be comparable. - You have **already measured** the steady-state profile of a stable workload and can set eden, survivor and old sizes better than a general heuristic can infer them. - You are **diagnosing or fixing survivor collapse** or premature promotion and want the survivor ratio and tenuring threshold to stay where you put them. - The workload is **uniform and long-running**, so adaptation buys nothing and only adds variance. Keep it on for general-purpose services with unknown or shifting workloads — a heuristic that adapts usually beats a fixed guess. ## How to see what it is doing Unified logging exposes the ergonomics decisions: `-Xlog:gc+ergo*=debug` prints the goal being pursued and the resize decisions, alongside `-Xlog:gc+heap=debug` for the resulting sizes. Always confirm from the log rather than assuming your flags took effect. ## Interview framing "It resizes the generations and the tenuring threshold from measured statistics, pursuing pause goal, then throughput goal, then footprint — in that order. I leave it on for general services, and turn it off for benchmarks or when I've measured a stable profile and want deterministic sizing, especially if the survivor spaces are being squeezed into premature promotion."

  • You set -XX:MaxGCPauseMillis=50 on a Parallel-collector service and total throughput got worse. Why?
    Because the pause goal outranks the throughput goal. To shrink pauses the policy shrinks the generations, so each collection touches less data — but collections happen far more often, and the fixed per-collection costs (safepoint, root scanning, thread startup) are paid every time. The result is more total time in GC. On a throughput collector, an aggressive pause goal is usually the wrong instrument; if you genuinely need short pauses, change collector rather than squeeze this one.
  • How would you recognise survivor spaces being sized to nothing, and what is the consequence?
    In the GC log, young collections leave almost no data in the survivor space while old-generation occupancy climbs in visible steps after each young collection. The consequence is premature promotion: short-lived objects skip the survivor ageing process and land in the old generation, where only a full collection can reclaim them. The fix is to disable adaptive sizing and pin the survivor ratio and tenuring threshold, or to enlarge the young generation.

saying these in an interview costs you the question

  • Believing -XX:MaxGCPauseMillis is a hard limit rather than a goal the ergonomics steer toward
  • Getting the priority backwards — throughput does not outrank the pause goal
  • Setting -XX:SurvivorRatio while adaptive sizing is on and assuming it sticks
  • Thinking the policy tunes only the total heap size, missing the tenuring threshold and survivor sizing
  • Disabling adaptive sizing by default on services with unknown or shifting workloads, with nothing measured to replace it

context