Hook: Interviewers like this question because G1 looks simple on paper, but it reveals whether you understand how modern Java keeps pauses under control.
Question: How does G1 GC work?
Answer: G1, short for Garbage-First, is a region-based garbage collector that splits the heap into many small regions instead of treating it like one big block. It tries to keep pauses short by collecting the regions with the most garbage first, using mostly concurrent background work plus brief stop-the-world evacuation pauses. It is a generational collector, so it handles young objects frequently and old objects in selected mixed collections when that gives the best payoff.
Interview-Ready Answer: I’d explain G1 as a region-based, mostly concurrent, generational GC. The heap is split into equal-sized regions, usually 1 to 32 MB, and G1 first does concurrent marking to find where the garbage is, then during a pause it evacuates the regions with the most reclaimable space first. That gives it a way to aim for a pause target, like the default 200 ms from -XX:MaxGCPauseMillis, instead of chasing only throughput. On modern server JVMs, it has been the default collector since Java 9.
G1 stands for Garbage-First. Its main job is to reduce long stop-the-world pauses on medium and large heaps by working on the regions that give the best payoff first. The heap is split into many equal-sized regions, usually between 1 MB and 32 MB, and the JVM chooses the size automatically based on heap size.
Use G1 when you want a good balance of throughput and predictable pauses, especially for services with large heaps and latency-sensitive requests. It is often a strong default for backend systems, cache-heavy services, and mixed workloads. If your main goal is maximum throughput for batch jobs, Parallel GC can still win; if your goal is ultra-low latency, newer collectors like ZGC or Shenandoah may be better.
| Collector | Strength | Weakness |
|---|---|---|
| G1 | Balanced pauses | More overhead than Parallel |
| Parallel | Highest throughput | Longer stop-the-world pauses |
| CMS | Low pauses for its time | Fragmentation; removed in Java 14 |
-XX:MaxGCPauseMillis is a goal, not a promise. The default goal is 200 ms, and the JVM will do its best, but it may miss under heavy allocation pressure.Real-World Story: Imagine an e-commerce checkout service running with a 32 GB heap. Most requests create short-lived cart and pricing objects, so G1 is a good fit: young regions die quickly, and pauses stay short enough that checkout latency stays stable. One day, a team adds a cache of full request payloads but forgets to remove old entries, so long-lived objects start piling up in old regions.
At first, the logs show repeated Pause Young (Mixed) events, which means G1 is trying to reclaim both young and selected old regions. Then the mixed collections stop reclaiming much memory because the cache keeps those objects alive, and the JVM starts spending more CPU on marking and remembering cross-region pointers. Users see checkout slowdowns, then timeouts, and the logs may eventually show longer pauses or even To-space exhausted messages when G1 cannot find enough free regions to evacuate live objects safely. The bug is not that G1 is broken; it is that the object graph stayed live for too long, so the collector had less and less garbage to profitably remove.
import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Deque;
import java.util.IdentityHashMap;
import java.util.List;
import java.util.Set;
public class Main {
static class Obj {
final String name;
final int bytes;
final List<Obj> refs = new ArrayList<>();
boolean marked;
Obj(String name, int bytes) {
this.name = name;
this.bytes = bytes;
}
Obj link(Obj other) {
refs.add(other);
return this;
}
}
static class Region {
final String name;
final List<Obj> objects = new ArrayList<>();
Region(String name) {
this.name = name;
}
Region add(Obj obj) {
objects.add(obj);
return this;
}
int totalBytes() {
int sum = 0;
for (Obj obj : objects) {
sum += obj.bytes;
}
return sum;
}
int liveBytes() {
int sum = 0;
for (Obj obj : objects) {
if (obj.marked) {
sum += obj.bytes;
}
}
return sum;
}
double garbageRatio() {
int total = totalBytes();
if (total == 0) {
return 0.0;
}
return (total - liveBytes()) / (double) total;
}
}
static class ToyG1 {
final int pauseBudgetBytes;
final List<Region> regions;
final List<Obj> roots;
ToyG1(int pauseBudgetBytes, List<Region> regions, List<Obj> roots) {
this.pauseBudgetBytes = pauseBudgetBytes;
this.regions = regions;
this.roots = roots;
}
void collect(String label) {
clearMarks();
markFromRoots();
System.out.println();
System.out.println("=== " + label + " ===");
printRegions();
List<Region> collectionSet = new ArrayList<>();
// Toy rule: always collect young regions, then add old regions with lots of garbage.
for (Region region : regions) {
if (region.name.startsWith("Y")) {
collectionSet.add(region);
}
}
for (Region region : regions) {
if (region.name.startsWith("O") && region.garbageRatio() >= 0.50) {
collectionSet.add(region);
}
}
int estimatedEvacuationWork = 0;
for (Region region : collectionSet) {
estimatedEvacuationWork += region.liveBytes();
}
System.out.println("Chosen collection set: " + regionNames(collectionSet));
System.out.println("Estimated evacuation work: " + estimatedEvacuationWork + " bytes");
System.out.println("Pause budget: " + pauseBudgetBytes + " bytes");
if (estimatedEvacuationWork > pauseBudgetBytes) {
System.out.println("Result: pause target missed. In a real JVM, G1 may need a longer pause or can even hit evacuation failure if free regions run low.");
return;
}
int reclaimed = 0;
for (Region region : collectionSet) {
reclaimed += region.totalBytes() - region.liveBytes();
}
System.out.println("Result: collection succeeds, reclaimed about " + reclaimed + " bytes.");
}
private void clearMarks() {
for (Region region : regions) {
for (Obj obj : region.objects) {
obj.marked = false;
}
}
}
private void markFromRoots() {
// Identity-based set: the same object instance is what matters to GC.
Set<Obj> seen = java.util.Collections.newSetFromMap(new IdentityHashMap<Obj, Boolean>());
Deque<Obj> stack = new ArrayDeque<>(roots);
while (!stack.isEmpty()) {
Obj current = stack.pop();
if (!seen.add(current)) {
continue;
}
current.marked = true;
for (Obj next : current.refs) {
stack.push(next);
}
}
}
private void printRegions() {
for (Region region : regions) {
int total = region.totalBytes();
int live = region.liveBytes();
int garbage = total - live;
System.out.printf(" %s -> total=%d, live=%d, garbage=%d, garbageRatio=%.0f%%%n",
region.name, total, live, garbage, region.garbageRatio() * 100.0);
}
}
private String regionNames(List<Region> selected) {
List<String> names = new ArrayList<>();
for (Region region : selected) {
names.add(region.name);
}
return names.toString();
}
}
public static void main(String[] args) {
// Reachable chain from the root: these objects survive the marking phase.
Obj a = new Obj("A", 10);
Obj b = new Obj("B", 12);
Obj c = new Obj("C", 8);
a.link(b);
b.link(c);
// Another live chain that sits in an old region.
Obj d = new Obj("D", 18);
Obj e = new Obj("E", 6);
d.link(e);
// Unreachable objects: they are pure garbage and should be reclaimed.
Obj z = new Obj("Z", 4);
Obj f = new Obj("F", 7);
Obj g = new Obj("G", 20);
Obj h = new Obj("H", 9);
Obj i = new Obj("I", 9);
Region y1 = new Region("Y1").add(a).add(b).add(c).add(z);
Region y2 = new Region("Y2").add(f).add(g);
Region o1 = new Region("O1").add(d).add(e);
Region o2 = new Region("O2").add(h).add(i);
List<Region> regions = Arrays.asList(y1, y2, o1, o2);
List<Obj> roots = Arrays.asList(a, d);
new ToyG1(40, regions, roots).collect("Run 1: enough pause budget");
new ToyG1(15, regions, roots).collect("Run 2: tiny pause budget (failure path)");
}
}Follow-up & Tricky Questions:
-XX:MaxGCPauseMillis a hard guarantee? No. It is a goal that guides G1’s heuristics, but real allocation pressure and object lifetimes can make the JVM miss the target.-XX:MaxGCPauseMillis is a heuristic goal, not a hard deadline.Memory Hook: Think of G1 as a warehouse clerk who always cleans the messiest shelves first, one shelf at a time, instead of shutting down the whole warehouse.
-XX:MaxGCPauseMillis is a goal, default 200 ms.