HotShard
Java memory

G1: Cleaning a Piece at a Time

Cut the heap into regions, find the ones that are mostly garbage, and empty a few per pause, sized to the pause time you asked for.

Part 4 let the GC find every live object in the old generation while your program runs. But finding garbage doesn't free it: getting the space back still means moving live objects, with the program stopped, and compacting the whole old generation at once makes the pause grow with every gigabyte of live data. G1, the default collector on most machines, avoids that by cleaning a few pieces at a time.

~10 min read

Moving objects still freezes the program#

TL;DRthe 30-second version
  • G1 cuts the heap into equal regions; Eden, survivor and old are just sets of regions.
  • Concurrent marking counts live bytes per region, and mixed collections empty the regions with the most garbage first.
  • Each pause adds regions until its predicted time reaches your pause target (200 ms by default).
  • Remembered sets, filled by a cross-region write barrier, let G1 empty a region without scanning the heap. If empty regions run out, it falls back to a full GC.

Part 3's old-generation collection slid every live object together, with the program stopped, so the pause grew with the amount of live old data. Concurrent marking removed the marking part of that pause. The moving part remains.

The idea is simple: instead of cleaning the whole old generation in one long pause, clean a few pieces of it in each short pause. But for that, the heap has to be made of pieces that can be cleaned on their own.

Regions#

G1 cuts the heap into many equal-sized regions, typically 1 to 32 MB each, picked so the heap has around two thousand of them. An 8 GB heap might be 2,048 regions of 4 MB. At any moment, each region has one job:

[E][O][E][ ][O][S][O][ ][E][O][O][ ][H][H][O][E] ...
 E = Eden   S = Survivor   O = Old   H = Humongous   [ ] = empty

So the young and old generations are still there, but each is just a set of regions, scattered anywhere in the heap. A young collection works as before, only on regions: when the Eden regions fill, G1 pauses briefly and copies the live objects out of every Eden and survivor region into a few empty regions. Then all the regions it copied out of are empty again, in one step. Copying out of a region like this is called evacuation.

Garbage first#

Old regions slowly fill with promoted objects. Once the old generation passes a set share of the heap (45% by default, adjusted as G1 learns), G1 starts concurrent marking, as in Part 4, and counts how many live bytes are in each old region. Regions that turn out completely empty are freed right away, without moving anything. Most, though, are a mix. Compare two 4 MB regions:

RegionLive dataCost to empty itNet space gained
X5%copy 0.2 MB3.8 MB (4 MB freed, 0.2 MB of copies)
Y90%copy 3.6 MB0.4 MB (4 MB freed, 3.6 MB of copies)

The copies need space too, so emptying X gains almost ten times as much as emptying Y, for one eighteenth of the copying. So G1 empties the regions with the most garbage first, and by default it skips regions that are more than 85% live altogether, which is where the name comes from: Garbage-First. After marking, the next few pauses each empty all the young regions plus a handful of the old regions with the least live data. Those are called mixed collections, and pause by pause they clean the old generation, cheapest pieces first.

You also set how long each pause should take, with -XX:MaxGCPauseMillis (default 200 ms). G1 learns from earlier pauses how long copying a region tends to take, and adds regions to each pause until the predicted time reaches your target. So the pause follows your target, not your heap size. It's a prediction, though, so a pause can still run over.

PredictYou lower MaxGCPauseMillis from 200 ms to 50 ms. What changes?

Each pause empties fewer regions, so pauses get shorter but more frequent, and the old generation is cleaned more slowly. Set it too low and G1 may not keep up, which leads to a full GC.

Who points into this region?#

Emptying region X means updating every reference to the objects that move out of it, and those references could be anywhere in the heap. Scanning the whole heap on every pause would wipe out the benefit. It's Part 3's blind spot, only wider: not "who in the old generation points into the young one?" but "who anywhere points into this region?"

The fix is the same idea: a write barrier, this time after every reference store, with one check:

// you wrote:  obj.field = value;
obj.field = value
if (value != null and region_of(obj) != region_of(value))
    record "this card holds a reference into region_of(value)"

Background threads later take those records and file each one under the region it points into. So every region G1 might empty keeps a remembered set: the places elsewhere in the heap that hold references into it. To empty region X, G1 scans only X's remembered set.

So in G1, one reference store can run two barriers. Before it, and only while marking runs, the SATB barrier from Part 4 records the old value. After it, always, the cross-region barrier records where the new reference points. That's part of the price of short pauses.

Humongous objects, full GCs, and the limit#

Huge objects, like a big array, would be expensive to copy. So any object of at least half a region is humongous: it gets its own run of consecutive regions, and normal collections don't move it. When it dies, its regions are simply freed.

Evacuating needs somewhere to copy into, so G1 always needs some empty regions. If the program allocates faster than G1 cleans, or marking starts too late, they run out, and G1 falls back to a full GC: stop everything and compact the entire heap, the long pause all of this was designed to avoid. Full GCs in a G1 log usually mean the heap is too small or the allocation rate is too high for the collector to keep up.

And one limit remains. The copying itself still happens with the program stopped, so every pause has to copy at least the young survivors plus the old regions it chose. To go below that floor, a collector would have to move objects while the program keeps running. That's Part 6.

What G1 trades#

You getYou pay
Pauses sized to a target, not to the heapTwo write barriers on reference stores
The old generation cleaned a piece at a time, cheapest firstMemory for remembered sets, and background threads to maintain them
Marking that runs alongside your programA fallback full GC if it can't keep up

If this comes up in an interview#

The one-linerG1 splits the heap into equal regions so it can clean a few at a time. Concurrent marking counts live data per region, and mixed collections empty the regions with the most garbage first, sized to a pause-time target. Remembered sets, filled by a cross-region write barrier, let it empty a region without scanning the heap. If it runs out of empty regions, it falls back to a full GC.
Why is it called Garbage-First?

Because after marking it empties the regions with the most garbage (least live data) first: the most space freed per byte copied.

What is a mixed collection?

A G1 pause that evacuates all young regions plus some old regions chosen by how little live data they hold.

What is a remembered set?

A per-region record of the places elsewhere in the heap that reference it, so the region can be emptied without scanning the whole heap.

Gotchas
  • Many large arrays just over half a region each waste nearly half a region apiece. Humongous allocations also show up in GC logs; a larger region size (-XX:G1HeapRegionSize) can help.
  • A very low pause target makes G1 clean too slowly and can trigger full GCs.
  • Full GCs in a G1 log are a symptom, not a setting to tune away: give the heap more room, or allocate less.
Under the hood
  • When an evacuation can't find space for an object, G1 can leave it in place for that pause (evacuation failure) before resorting to a full GC.
  • G1's full GC is a parallel, stop-the-world mark-compact of the whole heap.
  • The details of how barrier records reach the remembered sets have changed across JDK releases; the idea, a filtered post-write barrier feeding per-region remembered sets, has not.
References & further reading
References

Feedback on this topic →