skip to content

How does a JVM decide the exact age at which a surviving object is promoted into the old generation? Explain both the configured maximum tenuring threshold and the threshold the collector computes at runtime, and how you would observe the actual age distribution.

level: middleimportance: should knowfreq 48%

answer

  1. MaxTenuringThreshold = ceiling, default 15
  2. 4 header bits ⇒ 15 is the hard maximum
  3. adaptive: sum ages until TargetSurvivorRatio (50%) exceeded
  4. -Xlog:gc+age=trace shows the real histogram
  5. threshold pinned at 1 ⇒ survivors too small

basics

~20 s

An object is promoted when its age reaches the tenuring threshold. -XX:MaxTenuringThreshold sets the ceiling (default 15, the maximum the header age bits allow). Each collection the JVM also computes a lower effective threshold: it sums survivors by age until the survivor space's target occupancy is exceeded, and tenures from that age up. -Xlog:gc+age=trace prints the distribution.

solid answer

~60 s

Promotion is age-driven. The age lives in the object header and is incremented on each copy; `-XX:MaxTenuringThreshold` (default 15 on the mainstream collectors, and 15 is the hard ceiling because the header field is 4 bits) is the *maximum* age an object may reach in the survivor spaces. The threshold actually applied is usually lower and is recomputed every collection. The collector builds a histogram of surviving bytes by age, then accumulates from age 1 upward until the total exceeds the target occupancy of the survivor space — `-XX:TargetSurvivorRatio`, by default 50% of the survivor space. The age at which it overflows becomes the effective threshold for that collection; everything at or above it is tenured. The intent is to leave headroom so the next collection's survivors still fit. So if survivors are large relative to the survivor space, the threshold collapses towards 1 and objects tenure almost immediately. `-Xlog:gc+age=trace` (formerly `-XX:+PrintTenuringDistribution`) prints the histogram and the chosen threshold each collection — that log, not the flag value, tells you what is really happening.

code

text · 4 lines
text
[gc,age] Desired survivor size 8388608 bytes, new threshold 1 (max 15)
[gc,age] - age   1:   14680064 bytes,   14680064 total
[gc,age] - age   2:     524288 bytes,   15204352 total
[gc,age] - age   3:      65536 bytes,   15269888 total

go deeper

for a junior

Know that promotion happens at an age threshold, that the default ceiling is 15, and that the age is counted in young collections survived.

for a middle

Explain the adaptive computation — survivors summed by age against the survivor-space occupancy target — and name the logging option that reveals it.

for a senior

Read a real age histogram and derive a conclusion from it: a threshold pinned at 1 with a large age-1 bucket means undersized survivors and imminent old-generation pressure.

for a principal

Argue the tradeoff explicitly — survivor capacity buys filtering of medium-lived data at the price of longer young pauses and less eden — and decide whether the workload's lifetime distribution justifies paying it at all.

## The two numbers people confuse There are two distinct thresholds and interview answers routinely merge them: - **The configured maximum** — `-XX:MaxTenuringThreshold`. This is a ceiling, not a target. Its default is 15 on the mainstream generational collectors. The value 15 is not arbitrary: the age is stored in a small field of the object header's mark word (classically 4 bits), so 15 is the largest age representable. Setting it higher is silently clamped. - **The effective (adaptive) threshold** — recomputed at *every* young collection from the actual data. This is the number that decides promotions, and it is frequently far below the configured maximum. ## How the adaptive threshold is computed During evacuation the collector accumulates, per age bucket, how many bytes survived. After copying it walks the histogram from age 1 upward, adding up bytes, and compares the running total against the **desired survivor size**: the survivor space capacity multiplied by `-XX:TargetSurvivorRatio` (default 50%). The first age at which the running total exceeds that budget becomes the effective threshold; objects of that age and above are tenured on the next collection rather than copied to the survivor space. The logic is a capacity argument, not a longevity argument. The collector is not asserting that age-3 objects are long-lived; it is asserting that it does not have room to keep copying them and must move some data out of the way. Keeping survivor occupancy near half of capacity leaves slack for the next cycle's surviving eden data, which is not yet known. ## Reading the distribution The age histogram is printed by `-Xlog:gc+age=trace` on modern JVMs (the pre-JDK-9 spelling was `-XX:+PrintTenuringDistribution`). Each line is one age bucket with the bytes at that age and the running total, followed by the desired survivor size and the threshold chosen. Two patterns matter: - A histogram whose mass sits at ages 1–2 with the threshold pinned at 1, plus a desired-survivor-size line far below the total, means the survivor space is too small for the workload; data is being pushed into the old generation immediately. - A histogram with a long tail that reaches the maximum age means genuinely long-lived data is being copied 15 times before it tenures — pure pause-time overhead, since it was always destined for the old generation. ## Tuning levers and their tradeoffs Survivor capacity is controlled indirectly through `-XX:SurvivorRatio` (eden-to-survivor sizing) on the classic collectors, or emerges from region counts and the ergonomic young-size policy on region-based ones. Adaptive sizing policies may resize survivor spaces between collections on their own, which is why a flag you set may be overridden by ergonomics. Raising the threshold or enlarging survivors keeps medium-lived data out of the old generation, at the cost of longer young pauses (more bytes copied per collection) and less eden for a given young size (more frequent collections). Lowering the threshold hands data to the old generation quickly: young pauses shrink, old-generation pressure grows. There is no universally correct setting — the right answer depends on whether the application's object lifetime distribution has a genuine "medium-lived" hump that survivor spaces can absorb. Setting `MaxTenuringThreshold=0` forces every survivor to be tenured immediately, effectively deleting the survivor filter. It is occasionally used when *all* surviving data is known to be long-lived, but as a general setting it guarantees that short-lived data which merely lost the timing lottery ends up in the old generation. ## The pathological interaction The adaptive threshold and survivor overflow reinforce each other. A burst of allocation produces an unusually large survivor set; the threshold collapses to 1; nearly everything is tenured; the old generation fills with objects that would have died within a second or two. The old generation then needs collecting more often, and old-generation collections are the expensive kind. This is *premature promotion*, and the tell is precisely the collapsed threshold plus large promoted volumes in the age log — not any single flag value. ## Collector-specific notes Region-based collectors keep the same age-and-threshold model but implement survivor spaces as sets of regions rather than fixed contiguous spaces, so the effective survivor capacity moves with each collection's sizing decisions. The generational modes of newer low-pause collectors likewise retain aging, though their promotion policies and the structures recording cross-generational references differ. In all of them the interview-relevant point is unchanged: the configured maximum is an upper bound, and the number that governs behaviour is computed from live data every cycle.

  • Why is 15 the maximum value of MaxTenuringThreshold?
    The age counter is stored in a small bit field of the object header's mark word — classically four bits — so ages 0 through 15 are all that can be represented. Values above 15 are clamped rather than honoured. The header is deliberately tiny because every object carries one, so the JVM will not spend more bits on aging.
  • What does MaxTenuringThreshold=0 do, and when might it be reasonable?
    It tenures every surviving object on its first young collection, bypassing the survivor filter entirely. It can be reasonable when profiling shows that essentially everything surviving a young collection is genuinely long-lived, so the survivor copying is pure overhead. In most workloads it is harmful, because objects that merely happened to be alive when the collection fired are promoted and then die in the old generation.

saying these in an interview costs you the question

  • Treating MaxTenuringThreshold as the age at which promotion always happens, rather than as a ceiling.
  • Believing the JVM never varies the threshold at runtime.
  • Assuming a larger MaxTenuringThreshold guarantees less promotion, ignoring that survivor overflow promotes regardless of age.
  • Claiming the age is stored per class or in a collector side table rather than in the object header.
  • Reasoning about promotion from flag values alone without ever looking at the age distribution log.

context