Java path · Objects and types · Topic 6 of 12

Garbage collection: generations, G1 and ZGC

Not started

The Java specifications promise only that unreachable objects are reclaimed automatically; they do not say how. HotSpot ships several collectors and picks G1 by default on most machines. All of them start from the same idea: an object is alive if it can be reached from a root, such as a local variable in some thread's stack frame or a static field, and everything else can be reclaimed. They differ in when they stop your threads and for how long.

Why it matters: Pause times, heap size and CPU spent collecting are trade-offs you choose with one flag. Choosing well starts with knowing what each collector does.

  1. 1Reachable means alive

    The collector starts from roots (thread stacks, static fields) and keeps everything it can reach. The rest is garbage, however it got there.

  2. 2Generations

    Most objects die young, so new objects go into a young generation that is collected often and cheaply; survivors are promoted to the old generation.

  3. 3G1 is the default

    G1 splits the heap into regions and aims for a pause-time goal (200 ms by default), collecting the regions with the most garbage first.

  4. 4ZGC for low latency

    ZGC does nearly all its work while your threads run, with pauses typically under a millisecond whatever the heap size.

1

How it works

The explanation, with examples you can run.

Generations

Most objects die young: the iterator in a loop, the StringBuilder behind a log message, the DTO for one request. HotSpot's collectors exploit this by splitting the heap:

  • New objects are allocated in the young generation (its eden space).
  • A young (minor) collection copies the few survivors out and treats the rest of eden as empty again. Its cost depends on how much is alive, not how much was allocated, which makes it cheap.
  • Objects that survive several young collections are promoted to the old generation, which is collected less often.

To collect the young generation alone, the collector must know about references from old objects to young ones. The JIT emits a small write barrier on every reference store to track them.

G1, the default

G1 (Garbage-First) divides the heap into equal regions, a power of two in size (a few megabytes for typical heaps), sized to give about 2048 of them. At any time a region is free, eden, survivor or old. Young collections are pauses that evacuate the live objects in eden and survivor regions into new ones. When the heap fills past a threshold, G1 marks live objects concurrently, while your threads run, and then does mixed collections that also evacuate the old regions with the most garbage: hence the name.

G1 sizes its work to meet a pause-time goal, -XX:MaxGCPauseMillis, 200 ms by default. It is a goal, not a guarantee. Objects of half a region or more are humongous and go straight into old regions, so many large short-lived arrays can make G1 work harder.

ZGC, for low latency

ZGC does marking, relocation and reference fixing concurrently, using load barriers: a small check on every reference read that fixes up a pointer if the object has moved. Its pauses are typically well under a millisecond and do not grow with the heap, from a few hundred megabytes to many terabytes. The price is some throughput and memory headroom: your threads keep allocating while it works, so give it a larger -Xmx.

In Java 21, -XX:+UseZGC selects the original single-generation ZGC; add -XX:+ZGenerational for the generational version from JEP 439, which handles high allocation rates better. Later releases made generational the default (Java 23) and then the only mode (Java 24).

The others, and what to measure

  • Serial: one thread, stop-the-world. Fine for small heaps and single-core containers, where HotSpot may pick it automatically.
  • Parallel: stop-the-world with many threads; the best throughput when pauses don't matter, as in batch jobs.

Turn on logging before tuning anything:

# HotSpot unified logging: one line per collection, with pause time and heap before and afterjava -Xlog:gc -Xmx2g -jar app.jar # or switch collectorjava -XX:+UseZGC -XX:+ZGenerational -Xlog:gc -Xmx8g -jar app.jar

Look at pause times, how often collections happen, and how full the old generation stays after each one. An old generation that keeps growing after every collection is a leak, not a tuning problem: something reachable, such as a static map used as a cache, is holding on to objects.

2

Check yourself

3 questions. Get them all right to finish this topic.

  1. 1.Why does a young-generation collection usually take little time?
  2. 2.Which garbage collector does the Java Language Specification require?
  3. 3.In Java 21, which flags give you generational ZGC?

0 of 3 right so far. Get all three to finish this topic.