Skip to content

Memory management & garbage collection

Senior

The generational heap — Eden, Survivor spaces, and the old generation — mark-sweep-compact, card tables, and the modern low-pause collectors.

Created 29 September 2026

5 concepts · detail hidden

  1. 01The generational heap — Eden, Survivor spaces, and the old generation

    The JVM heap is split by object age, driven by the same generational hypothesis behind CPython’s own generational GC (see “reference counting and generational GC” in the Python topic): most objects die young, so it’s far cheaper to collect the region holding young, short-lived objects frequently and cheaply, and only bother with the region holding long-lived objects occasionally. Concretely, the heap has two main generations: the young generation, split into Eden (where every new object is allocated first) and two equally-sized Survivor spaces, conventionally labeled S0 and S1 (you’ll also see them called “from” and “to” — at any moment exactly one is active); and the old generation (also called the tenured generation — informally, if you like, the “graveyard” for objects that have proven they’re going to stick around) which holds objects that have survived long enough in the young generation to be promoted out of it. Separately, since Java 8, class metadata (the runtime representation of loaded classes themselves, not the objects instantiated from them) lives in Metaspace — native, off-heap memory that grows dynamically, replacing the old fixed-size PermGen that used to cause a distinct OutOfMemoryError: PermGen space in class-loading-heavy applications (app servers doing frequent hot redeployment being the classic case).

    #c-the-generational-heap-eden-survivor-spaces-and-the-old-gener
  2. 02How a Minor GC actually moves objects — Eden, copying between Survivor spaces, and promotion

    Every new object is allocated in Eden, typically through a TLAB (Thread-Local Allocation Buffer) — each thread claims its own small private chunk of Eden up front and allocates into it with a simple, lock-free pointer bump, avoiding cross-thread synchronization for the overwhelmingly common case; a thread only falls onto a slower, synchronized allocation path once its TLAB fills up. When Eden itself fills, a Minor GC (a.k.a. Young GC) runs: it’s implemented as a copying collector rather than mark-sweep, because in the young generation the vast majority of objects are expected to be garbage — it’s cheaper to copy the small surviving minority somewhere else than to sweep and compact around the (much larger) dead majority. Concretely: every object in Eden still reachable from a GC root gets copied into the currently-active Survivor space (say S0); any object already living in the other Survivor space (S1) that’s still reachable also gets copied into S0; everything left behind in Eden and the now-empty S1 is simply abandoned as garbage, with no per-object sweeping needed — which Survivor space is “active” then flips for the next cycle. Each surviving object carries an age counter, incremented on every Minor GC it survives; once an object’s age crosses a tenuring threshold (JVM-tunable via -XX:MaxTenuringThreshold, or decided adaptively by default), it’s promoted directly into the old generation instead of continuing to bounce between Survivor spaces.

    #c-how-a-minor-gc-actually-moves-objects-eden-copying-between-s
  3. 03Major/Full GC in the old generation — mark, sweep, and compact

    The old generation holds objects expected to live a long time, so copying them around on every collection (the young generation’s strategy) would be wasteful — instead it’s collected with the classic mark-sweep(-compact) algorithm. The mark phase walks the live object graph starting from the GC roots (local variables and parameters currently on any thread’s stack, static fields, JNI references) and marks every object reachable from them. The sweep phase then reclaims the memory occupied by everything left unmarked. An optional compact phase slides the surviving objects together, eliminating the gaps sweep leaves behind — genuinely necessary in the old generation specifically, since it accumulates fragmentation over many collection cycles in a way the young generation’s copying approach never lets happen there, and compacting keeps subsequent allocation cheap (a simple bump-pointer, the same trick a TLAB uses) rather than requiring a slower free-list search through fragmented gaps. A full pass like this over the old generation (often the whole heap) is called a Major GC, or a Full GC when it collects everything — historically the most disruptive kind of pause, since with older collectors the entire application was frozen until it finished, precisely because compaction requires knowing no mutator thread is concurrently changing references while objects are being physically relocated.

    #c-majorfull-gc-in-the-old-generation-mark-sweep-and-compact
  4. 04Card tables and write barriers — how a Minor GC stays cheap despite old→young references

    A Minor GC only scanning the young generation’s own GC roots would miss a real case: an object living in the old generation that holds a reference to a newly-allocated object in the young generation (e.g., a long-lived cache object that just got handed a fresh entry) — that old-gen object needs to count as a GC root for the young generation’s reachability scan too, or the young object it references would be wrongly collected. Scanning the entire old generation on every single Minor GC to find such references would defeat the entire point of making young-gen collection cheap and frequent, so the JVM tracks this cheaply instead: the heap is conceptually divided into small, fixed-size cards (a few hundred bytes each), and a write barrier — a small extra bit of code the JIT inserts on every reference-field write — marks the containing card dirty whenever a reference is written from an old object into anything. A Minor GC then only needs to scan the (typically small) set of dirty cards to find genuine old→young references acting as extra roots, rather than walking the entire old generation — a direct, JVM-specific parallel to the “old objects pointing at young objects” problem any generational collector has to solve somehow.

    #c-card-tables-and-write-barriers-how-a-minor-gc-stays-cheap-de
  5. 05Other JVM GC optimizations — stop-the-world pauses, and the shift to concurrent, low-pause collectors

    Stop-the-world (STW): during (at least part of) a GC cycle, every application (“mutator”) thread is paused completely, so the collector can safely walk and move objects without the running program concurrently mutating references underneath it — how much of a given GC cycle actually happens stop-the-world, versus concurrently alongside the running application, is the single biggest practical difference between JVM collectors. G1 (Garbage-First), the default since Java 9, replaces the fixed contiguous Eden/Survivor/Old layout with many small, equally-sized heap regions that are dynamically assigned generational roles, tracks how much live data each region holds, and prioritizes collecting the regions with the most garbage first (where the name comes from) — its actual design goal is predictable, configurable pause times (-XX:MaxGCPauseMillis), not raw maximum throughput. ZGC and Shenandoah push further, targeting pause times in the low single-digit milliseconds even on multi-terabyte heaps, by doing almost all marking and object relocation work concurrently with the running application — using techniques like colored pointers and load barriers to let the collector move an object while a mutator thread might still be reading the old location — at the cost of somewhat higher CPU/throughput overhead, a tradeoff that’s usually worth it specifically for latency-sensitive production services where a multi-hundred-millisecond STW pause is unacceptable regardless of average throughput. Escape analysis — the JIT eliminating heap allocation entirely for an object it can prove never escapes a method, avoiding GC pressure altogether rather than merely optimizing how that pressure gets collected — is covered from the object-allocation side in the Python topic’s “what determines whether an object is stack- or heap-allocated,” which uses HotSpot’s version of exactly this optimization as its running example.

    # a representative young/old-generation tuning knob set, G1 (the current default)
    java -Xms4g -Xmx4g \
         -XX:+UseG1GC \
         -XX:MaxGCPauseMillis=200 \
         -XX:MaxTenuringThreshold=15 \
         -jar service.jar
    
    #c-other-jvm-gc-optimizations-stop-the-world-pauses-and-the-shi