skip to content

G1

regions, remembered sets, mixed collections. the default collector.

chapter
chapter 14.07
reading time
9 min read

G1 (garbage first) cuts the heap into about 2048 equal regions, and any region can be eden, survivor, old or humongous. young GCs evacuate the eden regions. when the old gen fills up, G1 marks it concurrently, then runs mixed GCs that also evacuate the old regions with the most garbage first.

it sizes all of that to hit a pause goal, -XX:MaxGCPauseMillis (200 ms by default). it's been the default collector since java 9, and since java 27 it's the default everywhere.

the picture

you know how Parallel has to compact the whole old gen at once, so a big heap means a big pause? G1's answer: never collect the whole old gen. collect pieces of it.

think of a warehouse made of identical shipping containers. you can't empty the whole warehouse in one night. so every night you check which containers are mostly trash, move the few useful boxes out of them into one empty container, and send the rest off to be recycled. how many containers you deal with each night depends on how long the night is (the pause target). containers that are full of useful stuff, you just leave alone.

pause target =
E eden0
S survivor0
O old0
H humongous0
free48

a fresh heap: every region is free. G1 has no fixed eden or old gen. any region can become either, and a region's job can change after every GC.

live data in a regiongarbage in a regioncollection setbeing marked
step 1/11
step through one full G1 cycle. outlined in amber is the collection set: the regions this GC will evacuate. after marking, each old region shows its live and garbage share. change the pause target and step to the mixed GC again: a tighter target means fewer old regions per pause.

see why it's called garbage first? after marking, G1 knows exactly how much garbage each old region holds, and it always evacuates the worst ones first: the least copying for the most space back. and since everything gets evacuated (copied into a fresh region), the old gen gets compacted a few regions at a time. no fragmentation, no giant compaction pause.

one real cycle

here's a real G1 cycle, straight out of -Xlog:gc for the toy service from the last chapter (256 MB heap, 1 MB regions, jdk 25):

app stopped
concurrent GC
30 ms70 ms110 ms
hover or tap a block to see its log line.
stop-the-world pauseconcurrent GC work, app keeps running
hover the blocks. every young and mixed GC is a short stop-the-world pause. marking runs concurrently in between, interrupted only by the tiny remark and cleanup pauses.

the lines it's made of, in the order they happen:

log sayswhat's happening
Pause Young (Normal)a plain young GC: evacuate eden and survivor regions.
Pause Young (Concurrent Start)the old gen crossed the threshold, so this young GC also starts concurrent marking (it scans the roots while the app is stopped anyway).
Concurrent Mark Cyclemarking, on GC threads, while the app runs.
Pause Remarkfinish marking (drain the SATB queues), process weak references, free regions that turned out to be completely empty, and pick the old regions worth collecting: the candidates.
Pause Cleanupa tiny pause that closes the cycle, after the candidates' remembered sets have been rebuilt concurrently.
Pause Young (Prepare Mixed)the last young-only GC before the mixed ones.
Pause Young (Mixed)young regions plus some old candidate regions.

look at the remark line: 201M->135M. that 66 MB came back without copying a single object: whole regions that marking found to be 100% garbage are simply freed.

and here's G1 deciding what goes into one mixed GC (-Xlog:gc+ergo+cset=debug):

text
GC(8) Start adding marking candidates to collection set. Min 8 regions, max 26 regions,
      available 58 regions (11 groups), time remaining 195.70ms, optional threshold 39.14ms
GC(8) Finish adding marking candidates to collection set (Maximum number of regions reached).
GC(8) Pause Young (Mixed) (G1 Evacuation Pause) 198M->166M(256M) 1.467ms

58 old regions were worth collecting. G1 wants to get through them in at most 8 mixed GCs (-XX:G1MixedGCCountTarget=8), so at least 8 per pause. it will never take more than 10% of the heap's regions in one go (26 of 256). the predicted time was nowhere near the 200 ms goal, so the region cap is what stopped it. the actual pause: 1.5 ms.

quick check
what does -XX:MaxGCPauseMillis=50 actually do?

remembered sets

collecting a handful of regions has the same problem as a young GC: objects outside the collection set might point in, and those pointers are roots. G1 can't scan the rest of the heap to find them, so every region keeps a remembered set: the list of cards (512-byte chunks) elsewhere in the heap that contain a pointer into it.

old region 7cardsold region 19cardseden region 31objremembered set of 31region 7, card 5region 19, card 2collecting region 31?scan 2 cards, not 2 regions.
an eden region with two incoming references from old regions. its remembered set (purple) lists exactly the two cards those references live in.

those sets are kept up to date by the post-write barrier. on a reference store, it checks whether the store crosses regions (and isn't null). if it does, it marks the card dirty and queues it, and background refinement threads move the card into the right remembered set. add the SATB pre-barrier from how GC works and that's G1's whole barrier story. it's also why G1 has a bit less throughput than Parallel: every reference store does a little extra work. java 26 cut that work down a lot (JEP 522): the app now just dirties a card in one table while refinement works on a second one, and the barrier went from around 50 instructions to 12 on x64.

the knobs worth knowing

flagdefaultwhat it does
-XX:MaxGCPauseMillis200the pause goal. the one knob G1 is designed around.
-Xmx / -Xms25% / 1/64 of RAMheap size. more heap, fewer GCs.
-XX:InitiatingHeapOccupancyPercent45when to start marking. G1 adapts it at runtime from what it observes.
-XX:G1HeapRegionSizeergonomic1 to 32 MB, aiming for ~2048 regions. bigger regions, fewer humongous objects.
-XX:G1ReservePercent10heap kept free as room to evacuate into.
-XX:G1MixedGCCountTarget8spread the old candidates over this many mixed GCs.
-XX:G1HeapWastePercent5stop mixed GCs once what's left would free less than this.
one level deeper:humongous objects, up close+

an object at least half a region in size is humongous. it gets its own run of contiguous regions straight in the old gen, and the tail of the last region is wasted. humongous allocations can also kick off a marking cycle early (you'll see (G1 Humongous Allocation) as the GC cause). short-lived humongous objects are less bad than they used to be: G1 can reclaim a dead one at any young GC if nothing points at it. but if -Xlog:gc+heap keeps showing Humongous regions: 40->40, either your buffers are oversized or -XX:G1HeapRegionSize is too small for them.

keylines
  • G1 splits the heap into equal regions. any region can be eden, survivor, old or humongous.
  • every collection evacuates whole regions, which also compacts as it goes.
  • concurrent marking finds out how much garbage each old region has. mixed GCs take the worst ones first.
  • remembered sets let G1 collect a few regions without scanning the rest of the heap.
  • MaxGCPauseMillis is a goal. don't pin the young gen, and treat a full GC in the log as a bug.