Introduction
The G1 (Garbage-First) garbage collector shipped experimentally in Java 6 Update 14 in July 2009, became officially supported in Java 7 (July 2011, JEP 326 by Jon Masamitsu and the HotSpot GC team), and replaced Parallel GC as the default collector in Java 9 (September 2017, JEP 248 by Monica Beckwith and Staffan Friberg). Unlike the older generational collectors that used fixed-size contiguous young and old generations, G1 partitions the heap into equal-sized regions and dynamically assigns each region a role (eden, survivor, old, or humongous). For a Java learner, the G1 GC algorithm matters because it is the collector your Java 9+ JVM uses by default, because it is the first mainstream collector to offer a configurable pause-time target, and because the G1 pause model differs fundamentally from the older CMS collector.
Understanding the G1 GC also explains the trade-offs between throughput, pause time, and footprint that production Java services face. Interviewers ask about G1 because it sits at the same conceptual level as the Shenandoah and ZGC collectors (low-pause collectors that followed G1) and because tuning G1 region size and pause-time target is a routine task for senior Java engineers running large heaps. The G1 algorithm is documented in the HotSpot source (src/hotspot/share/gc/g1/) and in the JEPs that introduced each incremental improvement.
What is G1 GC?
G1 GC is a server-style, region-based, mostly-concurrent, generational garbage collector with a soft real-time pause-time target. The heap is partitioned into 1MB to 32MB regions (default is the largest power of 2 that gives about 2048 regions), and a region can serve as eden, survivor, or old at any time. Young collections (young GC pauses) collect only the young regions (eden plus survivor) and are stop-the-world; they evacuate live objects into new survivor or old regions. Mixed GC pauses additionally collect a subset of old regions chosen by their garbage density (the garbage-first heuristic). Concurrent marking runs in the background between mixed GC pauses to identify live old regions.
G1 is categorised as a regional mostly-concurrent collector, in contrast to the older Parallel collector (throughput-oriented, generational, fully STW) and the older CMS collector (mostly-concurrent, generational, fragmentation-prone). The defining feature of G1 is the pause-time target: you specify -XX:MaxGCPauseMillis=200 (the default), and G1 adjusts the number of regions collected per mixed pause to try to meet that target. The trade-off is that G1 uses more CPU overall than Parallel GC for the same throughput, and it requires more memory for Remembered Sets and Collection Sets.
How G1 GC Works - Step by Step
Walk through a single G1 collection cycle on a 4GB heap with 2048 regions of 2MB each. Suppose the JVM has been running for ten minutes, allocating about 50MB per second, with a pause-time target of 200ms. Here is exactly what happens.
Step 1: Allocation. Newly allocated objects go into the current eden region pointed to by the TLAB (Thread Local Allocation Buffer). When an eden region fills, G1 picks another free region as the next eden. Young regions grow without bound until the configured -XX:G1NewSizePercent (default 5%) to -XX:G1MaxNewSizePercent (default 60%) range hits.
Step 2: Young GC pause. When the young generation occupies -XX:G1NewSizePercent of the heap or the heap is approaching -XX:InitiatingHeapOccupancyPercent (default 45%), G1 triggers a young GC. The pause scans GC roots (thread stacks, global references, JNI handles), iterates the Remembered Sets of young regions to find old-to-young references, copies surviving young objects into new survivor or old regions (evacuation), and updates all references.
Step 3: Concurrent marking. After the young GC, if heap usage is above IHOP, G1 starts a concurrent marking cycle. The Initial Mark phase piggy-backs on a young GC pause to record the roots; the Concurrent Mark phase walks the live object graph in the background while application threads continue; the Remark phase (STW) processes SATB (Snapshot-At-The-Beginning) buffers to handle concurrent modifications; the Cleanup phase (STW) identifies completely empty old regions and counts garbage in partially empty ones.
Step 4: Mixed GC pauses. After the concurrent cycle, G1 starts collecting old regions in the young GC pauses; these are called mixed GCs. G1 ranks old regions by their reclaimable garbage (the garbage-first heuristic) and includes enough of them in each mixed pause to try to hit the pause-time target. Mixed pauses continue until the garbage density of the remaining old regions drops below -XX:G1MixedGCLIveThresholdPercent (default 85%).
Step 5: Evacuation failure and Full GC. If a mixed GC cannot find free regions to evacuate into (because the heap is nearly full and the live objects in the chosen regions are larger than the available free space), G1 reports "to-space exhausted" and falls back to a single-threaded Full GC that compacts the entire heap. This is the G1 failure mode and should be avoided by sizing the heap and tuning IHOP.
Step 6: Humongous objects. Any object larger than half a region (default: more than 1MB on a 2MB region) is allocated as a humongous object spanning multiple contiguous regions labelled H. Humongous regions are reclaimed during concurrent marking if they have no live references, which avoids copying them during evacuation.
Java Program for Java Garbage Collection G1
package com.example.gc;
import java.lang.management.GarbageCollectorMXBean;
import java.lang.management.ManagementFactory;
import java.lang.management.MemoryMXBean;
import java.lang.management.MemoryUsage;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.TimeUnit;
public class G1GCDemo {
static long[] allocations = new long[10_000];
static int next = 0;
public static void allocateGarbage(int mb) {
for (int i = 0; i < mb; i++) {
byte[] block = new byte[1024 * 1024];
block[0] = (byte) (i & 0xFF);
block[block.length - 1] = (byte) (i & 0xFF);
allocations[next % allocations.length] = block.length;
next++;
}
}
public static String describeGc(MemoryUsage heap, MemoryUsage nonHeap) {
return String.format(
"Heap: used=%,d KB, committed=%,d KB, max=%,d KB%n"
+ "NonHeap: used=%,d KB, committed=%,d KB, max=%,d KB%n",
heap.getUsed() / 1024,
heap.getCommitted() / 1024,
heap.getMax() / 1024,
nonHeap.getUsed() / 1024,
nonHeap.getCommitted() / 1024,
nonHeap.getMax() / 1024);
}
public static void printGcStats() {
MemoryMXBean memory = ManagementFactory.getMemoryMXBean();
System.out.println(describeGc(memory.getHeapMemoryUsage(),
memory.getNonHeapMemoryUsage()));
List<GarbageCollectorMXBean> gcs =
ManagementFactory.getGarbageCollectorMXBeans();
System.out.println("GC beans:");
for (GarbageCollectorMXBean gc : gcs) {
long count = gc.getCollectionCount();
long time = gc.getCollectionTime();
System.out.printf(" %s: count=%d, time=%d ms%n",
gc.getName(),
(count == -1 ? 0 : count),
(time == -1 ? 0 : time));
}
}
public static void main(String[] args) throws Exception {
System.out.println("=== Initial state ===");
printGcStats();
System.out.println("=== Allocating 200 MB of short-lived garbage ===");
long t0 = System.nanoTime();
List<byte[]> live = new ArrayList<>();
for (int i = 0; i < 200; i++) {
byte[] b = new byte[1024 * 1024];
b[0] = (byte) i;
live.add(b);
if (i % 50 == 0) {
allocateGarbage(50);
}
}
long t1 = System.nanoTime();
System.out.printf("Allocation took %d ms%n",
TimeUnit.NANOSECONDS.toMillis(t1 - t0));
printGcStats();
System.out.println("=== Releasing the live list ===");
live.clear();
live = null;
System.out.println("=== Triggering a full GC via System.gc() ===");
long tg0 = System.nanoTime();
System.gc();
long tg1 = System.nanoTime();
System.out.printf("System.gc() took %d ms%n",
TimeUnit.NANOSECONDS.toMillis(tg1 - tg0));
printGcStats();
System.out.println("=== Tuning hints ===");
System.out.println("Run with: java -XX:+UseG1GC "
+ "-Xmx512m -Xms512m -XX:MaxGCPauseMillis=200 "
+ "-XX:+PrintGCDetails "
+ "com.example.gc.G1GCDemo");
System.out.println("On Java 9+ UseG1GC is the default; "
+ "the flag is shown here for clarity.");
}
}
Code Explanation - Line by Line
The allocateGarbage method allocates 1MB byte arrays in a loop, writes to the first and last bytes to ensure the JVM does not eliminate the allocation as dead code, and stores the length in a circular buffer so a fraction of the allocations are reachable from a static root. The circular buffer trick mimics real-world short-lived object churn: most allocations become garbage immediately while a small fraction lives long enough to be promoted to old regions.
The describeGc method formats the heap and non-heap MemoryUsage returned by MemoryMXBean.getHeapMemoryUsage and getNonHeapMemoryUsage. MemoryUsage exposes four numbers: used (live bytes), committed (currently allocated by the OS), max (the upper bound -Xmx for heap, -XX:MaxMetaspaceSize for non-heap), and init (initial size). The printGcStats method iterates the GarbageCollectorMXBean list which exposes one bean per collector; on a Java 9+ JVM with the default G1 setup you see one bean named "G1 Young Generation" and one named "G1 Old Generation" or "G1 Concurrent GC".
The main method captures the initial state, allocates 200MB of long-lived data interleaved with 200MB of garbage via allocateGarbage, prints intermediate stats, clears the live list to make it reclaimable, then calls System.gc(). The System.gc() call is a hint that G1 treats as a request for a full collection; the time it took is printed so you can see that even a System.gc on G1 is several milliseconds. The final block prints the recommended command line for running the demo with explicit G1 tuning flags.
Interview Questions and Answers
Q1. What is the G1 garbage collector and when did it become the default?
G1 (Garbage-First) is a server-style region-based mostly-concurrent generational collector that targets a soft real-time pause bound. It shipped experimentally in Java 6u14 (2009), became officially supported in Java 7 (2011, JEP 326), and became the default collector in Java 9 (September 2017, JEP 248). It replaced Parallel GC because G1 produces shorter pauses on large heaps while still delivering competitive throughput.
Q2. What is a G1 region and how big is it?
A G1 region is a fixed-size contiguous chunk of the heap (between 1MB and 32MB, defaulting to the largest power of 2 that yields about 2048 regions per heap). Each region can serve as eden, survivor, old, or humongous at any time, which lets G1 defragment without compacting the entire heap. Region size is configured with -XX:G1HeapRegionSize and is typically chosen automatically based on -Xmx.
Q3. What is the difference between a young GC pause and a mixed GC pause in G1?
A young GC pause collects only the young regions (eden plus survivor) and is always stop-the-world. A mixed GC pause additionally collects a subset of old regions chosen by the garbage-first heuristic (the regions with the most reclaimable garbage first). G1 only schedules mixed pauses after a concurrent marking cycle has identified the live old regions; mixed pauses continue until the remaining old regions have low enough garbage density.
Q4. What is the role of -XX:MaxGCPauseMillis and how does G1 achieve it?
MaxGCPauseMillis is a soft target for the maximum duration of any single GC pause; the default is 200ms. G1 achieves it by collecting a subset of regions per pause, where the subset size is set so the predicted pause time (based on historical GC cost per region) is below the target. The target is soft, not hard: G1 may still exceed it if the remembered sets are large or if evacuation fails, but it is the primary tuning knob for pause-sensitive workloads.
Q5. What is a humongous object in G1 and why is it a problem?
A humongous object is any object larger than half a region; it is allocated as a contiguous span of humongous regions labelled H. Humongous allocation is a problem because humongous regions cannot be used for anything else while the object is live, fragmentation can occur when humongous objects of different sizes are freed and the freed spans are not contiguous, and the older path of reclaiming humongous regions requires a full concurrent cycle. Tuning region size up (e.g. -XX:G1HeapRegionSize=16m) can help by making typical large objects non-humongous.
Q6. What is -XX:InitiatingHeapOccupancyPercent and what does it control?
IHOP (default 45%) is the heap occupancy threshold above which G1 starts a concurrent marking cycle after the next young GC. Lowering IHOP starts cycles earlier, giving G1 more time to finish marking before the heap fills and reducing the chance of mixed-GC pauses being too long because there is not enough old garbage to reclaim. Raising IHOP reduces cycle frequency and saves CPU on marking threads. Java 12+ introduced adaptive IHOP (-XX:+G1UseAdaptiveIHOP, default on) which dynamically adjusts the threshold based on past cycle timing.
Q7. Your service has a 32GB heap, runs on Java 21, and the application logs show sporadic 4-second G1 mixed GC pauses that miss your 200ms SLA. Walk through how you would diagnose and fix this.
First, capture GC logs with -Xlog:gc*:file=gc.log:time and load them in GCEasy or JFR to confirm whether the pauses are mixed pauses, young pauses, or Full GCs. If they are mixed pauses, the root cause is usually that the CSet is too large because the historical cost predictor underestimates; lower -XX:G1MixedGCCountTarget (default 8) to spread the old-region reclamation across more pauses, raise -XX:G1MixedGCLIveThresholdPercent only if needed, and confirm that -XX:MaxGCPauseMillis is set explicitly rather than relying on the default. If the pauses coincide with humongous allocations, increase -XX:G1HeapRegionSize to keep large objects non-humongous. If the long pauses are Full GCs from evacuation failure, the heap is undersized or IHOP is too high; raise -Xmx or lower -XX:InitiatingHeapOccupancyPercent. As a last resort for ultra-low-pause workloads, switch to ZGC (-XX:+UseZGC) which gives sub-millisecond pauses on any heap size, at the cost of slightly lower peak throughput.
Sample Input and Output
Input: $ java -XX:+UseG1GC -Xmx512m -Xms512m -Xlog:gc:stderr:time com.example.gc.G1GCDemo Output (key lines): === Initial state === Heap: used=12,348 KB, committed=524,288 KB, max=524,288 KB NonHeap: used=4,521 KB, committed=6,144 KB, max=-1 KB GC beans: G1 Young Generation: count=2, time=15 ms G1 Old Generation: count=0, time=0 ms === Allocating 200 MB of short-lived garbage === [2024-01-15T10:23:01.123-0500] GC(3) Pause Young (Normal) (G1 Evacuation Pause) 256M->120M(512M) 18.123ms Allocation took 4123 ms Heap: used=120,300 KB, committed=524,288 KB, max=524,288 KB GC beans: G1 Young Generation: count=15, time=185 ms G1 Old Generation: count=0, time=0 ms === Releasing the live list === === Triggering a full GC via System.gc() === [2024-01-15T10:23:05.456-0500] GC(20) Pause Full (System.gc()) 120M->18M(512M) 87.456ms System.gc() took 87 ms Heap: used=18,423 KB, committed=524,288 KB, max=524,288 KB GC beans: G1 Young Generation: count=15, time=185 ms G1 Old Generation: count=1, time=87 ms === Tuning hints === Run with: java -XX:+UseG1GC -Xmx512m -Xms512m -XX:MaxGCPauseMillis=200 ... On Java 9+ UseG1GC is the default; the flag is shown here for clarity.
The output shows the heap growing as the demo allocates, several young GC pauses firing during allocation, and finally a Full GC triggered by System.gc() that reclaims nearly all the live data after the list was cleared. The GC bean counts increase over time, and the G1 Old Generation bean shows one Full GC after System.gc was called.
Common Mistakes to Avoid
Setting -XX:MaxGCPauseMillis too low is a common mistake: a target like 10ms on a multi-GB heap causes G1 to collect very few regions per pause, which inflates total GC count and may trigger Full GCs because old regions are not reclaimed fast enough. A target of 100ms-200ms is reasonable for most services; lower targets require ZGC or Shenandoah.
Forgetting to size -Xms equal to -Xmx on production heaps is a costly mistake: if -Xms is smaller than -Xmx, G1 grows the heap gradually and triggers full pauses whenever it grows; this shows up as periodic latency spikes. Always set -Xms=-Xmx for production G1 services unless you have an explicit reason not to.
Treating System.gc() as a no-op in production is wrong: by default the JVM honours System.gc() and runs a Full GC, which on G1 compacts the entire heap and is the worst possible pause. Either disable with -XX:+DisableExplicitGC or audit your dependencies for libraries that call System.gc() (older JDK code, some caching libraries, some JNI code).
Finally, ignoring the IHOP threshold is a common production bug: on a slowly growing heap, the default 45% IHOP may trigger marking too late, causing mixed GC pauses to be back-to-back and large. Either lower IHOP explicitly or trust adaptive IHOP (default on since Java 12) by not pinning the static flag.
Real-World Applications
G1 GC is the default collector on Java 9 through Java 21 for any heap that does not have an explicit -XX:+Use... override. Most enterprise Java services (Spring Boot apps on Kubernetes, Apache Kafka brokers, Elasticsearch nodes, Cassandra, Hadoop NameNode, HBase RegionServers) run on G1 by default with a 4GB to 32GB heap. G1 is also the typical collector for JVM-based databases like Apache Derby and for application servers (WildFly, Tomcat) running traditional Java EE workloads.
For heaps above 32GB or for ultra-low-pause requirements, production deployments increasingly switch to ZGC (Java 15+ production-ready, JEP 377 by Per Liden and Roman Kennke) or Shenandoah (Java 12+, JEP 189 by Christine H Flood and the Red Hat GC team). Both offer sub-millisecond pauses regardless of heap size, at the cost of slightly lower throughput than G1. The G1 algorithm is still the right default for most workloads because its throughput is closest to the old Parallel collector while offering much better pause behaviour.
JFR (Java Flight Recorder, open-sourced in Java 11, JEP 328 by Markus Groenlund) and the Async Profiler both expose G1 internal events (region count, mixed pauses, humongous allocations, evacuation failure) for production diagnostics. The jcmd tool (jcmd <pid> GC.heap_info, jcmd <pid> GC.run) and the JConsole / VisualVM GUI tools all build on the same GarbageCollectorMXBean interface the demo program uses.
Conclusion
The G1 GC algorithm partitions the heap into equal-sized regions, performs young GC pauses that evacuate live objects, runs concurrent marking to identify reclaimable old regions, and uses mixed GC pauses to reclaim the densest old regions first while targeting a soft pause bound. The G1 collector is the default on Java 9 through Java 21 and the right choice for most server-side Java workloads with heaps up to 32GB. Next study the GC log format (-Xlog:gc*), then the JFR GC events, and finally the newer low-pause collectors ZGC and Shenandoah for heaps where G1 pauses still miss the SLA.
Comments
Comments
Post a Comment