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.jarLook 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.
Check yourself
3 questions. Get them all right to finish this topic.
0 of 3 right so far. Get all three to finish this topic.