skip to content

What events cause a JVM to start a young (minor) collection, and what causes it to fall back to a full collection of the whole heap?

level: middleimportance: must knowfreq 58%

answer

  1. young GC = Eden allocation failure
  2. young cost ∝ survivors, frequency ∝ allocation rate / young size
  3. full GC = promotion/evacuation failure, threshold cycle too late, metadata, System.gc()
  4. humongous / very large array allocations bypass Eden
  5. low-latency collectors: pacing → allocation stall instead of long pause

basics

~20 s

A young collection is triggered when allocation cannot be satisfied from the young space — Eden is full. A full collection happens when the old generation cannot accept what must be promoted or has no room, when the class-metadata area hits its threshold, or when something explicitly requests one; in modern collectors it is mostly a fallback after concurrent work fails to keep up.

solid answer

~60 s

**Young collection.** Allocation normally happens by bumping a pointer in a thread-local buffer in Eden. When a thread cannot get a new buffer because Eden is exhausted, the JVM triggers a young collection: live objects are evacuated to survivor space or promoted, and Eden becomes empty again. So young GC frequency is driven by allocation rate divided by young-space size, not by how much garbage exists. **Full / old-generation collection.** Several distinct causes: - The old generation has no room for objects being promoted — *promotion failure* or *evacuation failure*. - Old-generation occupancy crosses a threshold that starts a concurrent marking cycle; if that cycle does not finish before space runs out, the collector falls back to a stop-the-world full collection. - The class-metadata area reaches its threshold, so a collection runs to unload classes. - `System.gc()` is called — often by direct-ByteBuffer allocation failing to reserve native memory. - A very large (humongous) allocation that cannot be satisfied. In a healthy service, repeated full collections mean the concurrent cycle is starting too late or the live set genuinely does not fit.

code

text · 7 lines
text
[12.318s][info][gc] GC(41) Pause Young (Normal) (G1 Evacuation Pause) 512M->48M(1024M) 6.912ms
[19.004s][info][gc] GC(58) Pause Young (Concurrent Start) (G1 Humongous Allocation) 780M->501M(1024M) 9.220ms
[41.887s][info][gc] GC(93) Pause Full (G1 Compaction Pause) 1020M->870M(1024M) 1841.550ms
[52.010s][info][gc] GC(97) Pause Full (System.gc()) 640M->300M(1024M) 903.117ms

// The parenthesised cause is the trigger: normal Eden exhaustion, a humongous
// allocation, a fallback compaction, and an explicit request, respectively.

go deeper

for a junior

Say a young collection happens when the young space fills and a full collection when the old generation cannot take more, and that GC is triggered by allocation rather than by a timer.

for a middle

Name the distinct triggers — Eden exhaustion, promotion/evacuation failure, occupancy threshold starting a concurrent cycle, metadata threshold, explicit requests, oversized allocations — and state the frequency-versus-size relationship.

for a senior

Read the trigger out of a GC log line, distinguish 'cycle started too late' from 'live set too large', and identify hidden System.gc() callers such as direct ByteBuffer reservation.

for a principal

Reason about it as capacity planning: allocation rate and live-set size set the collection schedule, and the choice is between headroom, concurrent CPU cost and acceptable failure modes such as allocation stalls.

## The general rule: collection is allocation-driven A tracing collector does not run on a timer or because "there is garbage". It runs because an **allocation cannot be satisfied**, or because a heuristic predicts that it soon will not be. Everything below is a refinement of that idea. ## Young (minor) collection Object allocation is normally a pointer bump inside a thread-local allocation buffer carved out of the young space's Eden area. Fast path: bump the pointer, done. When the buffer is exhausted, the thread asks for another; when Eden itself cannot supply one, allocation *fails*, and that failure is the trigger for a **young collection**. The collection traces from the roots plus the remembered set that records old-to-young references, evacuates the surviving young objects into a survivor area or promotes older ones, and leaves Eden empty. Consequences worth stating in an interview: - **Frequency = allocation rate / usable young size.** Doubling the young generation roughly halves the number of young collections for the same allocation rate. - **Cost ≈ the surviving live set**, not the garbage. Dead objects cost nothing to collect in a copying collector — they are simply not visited. This is why a high allocation rate of short-lived objects can be surprisingly cheap. ## Old generation and full collections Several distinct events push work into the old generation or into a whole-heap collection, and being able to name them separately is what distinguishes a middle answer from a junior one. **1. Promotion / evacuation failure.** A young collection needs somewhere to put survivors that are old enough or that do not fit in a survivor area. If the old generation cannot accept them, the young collection fails partway. The recovery is expensive — typically a stop-the-world full collection, and in region-based collectors an evacuation failure with objects left in place. **2. Occupancy threshold starting a concurrent cycle.** Mostly-concurrent collectors do not wait until the old generation is full: they begin a concurrent marking cycle when occupancy crosses an initiating threshold, so that reclamation completes before space runs out. Modern collectors adapt this threshold from observed allocation rates. If marking plus reclamation cannot outrun allocation, the collector runs out of room and falls back to a stop-the-world full collection — the classic "concurrent mode failure" shape. **3. Class-metadata pressure.** Class metadata lives outside the Java heap and has its own threshold. Reaching it triggers a collection whose purpose is to unload unreachable classes and their loaders. An application that generates classes at run time — dynamic proxies, scripting engines, repeated redeployments — can drive collections this way even with a nearly empty Java heap. **4. Explicit requests.** `System.gc()` and `Runtime.getRuntime().gc()` request a collection. Two important non-obvious callers: direct `ByteBuffer` allocation invokes it when it cannot reserve native memory (hoping to run cleanup for unreachable buffers), and some RMI/distributed-GC machinery schedules it periodically. `-XX:+DisableExplicitGC` ignores such requests, and there are flags to service them with a concurrent cycle rather than a full stop-the-world collection. **5. Allocations that cannot use the normal path.** An object too large for the young space — a very large array — is allocated directly into the old generation or into dedicated humongous regions. If space for it cannot be found, that allocation itself triggers collection, potentially a full one. **6. Tooling and management operations.** Requesting a heap dump or invoking a diagnostic that requires a clean heap typically performs a full collection first. ## What the low-latency collectors change Collectors designed around very short pauses replace "collect when full" with **allocation-rate-driven pacing**: they start a cycle early enough that reclamation is predicted to finish before the heap fills, sometimes on a timer as well. Their failure mode is not a long pause but an **allocation stall** — threads are throttled or blocked until memory becomes available — or, at the extreme, a degenerate or full collection fallback. So on those collectors the diagnostic question shifts from "why is the full GC so long" to "why did the cycle start too late or run too slowly". ## Reading it operationally - Frequent young collections with a low survivor count are normal and often cheap; the lever is young size versus allocation rate. - Repeated full collections are almost always one of: live set genuinely near heap size (fix the leak or raise the heap), concurrent cycle starting too late (start it earlier), promotion pressure from objects surviving young collections in bulk, or an explicit `System.gc()` caller. - A collection with an empty-looking Java heap points at metadata or an explicit trigger, not at heap pressure.

  • If a young collection's cost is proportional to surviving objects, why does allocating huge numbers of short-lived objects often stay cheap?
    A copying young collector visits only live objects: it traces from the roots and remembered set, copies survivors, and then declares the whole Eden area free without touching the dead objects at all. So a workload that allocates heavily but keeps almost nothing pays mainly for more frequent collections, each of which finds very little to copy.
  • Which non-obvious callers can make System.gc() fire in an application that never calls it directly?
    Direct ByteBuffer allocation calls it when it cannot reserve native memory, hoping to make unreachable buffers' cleanup run and free native memory. Some distributed-GC machinery for remote references schedules periodic calls. Certain tooling and diagnostic operations, such as taking a heap dump, also force a collection first.
  • What does it mean when full collections keep occurring even though each one frees very little?
    It means the live set is close to the heap size — the collector reclaims almost nothing, so the next allocation triggers another full collection. This is the classic pre-OutOfMemoryError thrashing shape and is a capacity or retention problem, not a tuning problem: either the heap is too small for the working set or something is retaining objects that should be dead.

saying these in an interview costs you the question

  • Saying GC runs periodically on a timer, or 'when there is enough garbage'.
  • Believing System.gc() always forces an immediate full collection regardless of flags and collector.
  • Claiming young collection cost scales with the amount of garbage rather than with survivors.
  • Forgetting promotion or evacuation failure as a full-collection trigger and naming only 'the old generation is full'.
  • Assuming a collection can only be caused by Java heap pressure, missing class-metadata thresholds and explicit requests.

context