The Garbage First (G1) Garbage Collector in the JVM is designed to provide predictable pause times, but achieving that in production often means understanding its internal tuning knobs, not just letting it run on defaults.
Let’s see G1 in action. Imagine we have a Java application with a lot of short-lived objects being created and discarded. Without tuning, G1 might trigger a full garbage collection cycle too often, leading to long pauses where the application threads stop.
Here’s a simplified view of what happens during a G1 collection cycle. G1 divides the heap into regions. It identifies "young" regions (where new objects are allocated) and "old" regions. During a minor collection, it collects the young regions, promoting surviving objects to old regions. A major collection, which G1 tries to avoid or keep short, deals with the old regions. The goal is to clear out garbage efficiently.
The key to tuning G1 is influencing when and how it performs these collections, primarily by adjusting its target for pause times and the amount of heap it dedicates to different phases.
The most impactful parameter is -XX:MaxGCPauseMillis. This is G1’s goal, not a guarantee. Setting it to 200 (milliseconds) tells G1 to try and complete its pause within that timeframe.
java -Xms4g -Xmx4g -XX:MaxGCPauseMillis=200 -jar myapp.jar
This command starts our application with a 4GB heap and sets the target for GC pauses. G1 will then adjust its collection frequency and the amount of work it does in each cycle to meet this target.
Another crucial setting is -XX:G1HeapRegionSize. This determines the size of the individual regions G1 carves up the heap into. The default is often 1MB or 2MB, depending on the heap size. If you have a very large heap, increasing this can sometimes be beneficial, but it’s a trade-off.
java -Xms32g -Xmx32g -XX:MaxGCPauseMillis=200 -XX:G1HeapRegionSize=4m -jar myapp.jar
Here, we’ve increased the region size to 4MB for a 32GB heap. Larger regions can reduce the overhead of managing regions, but if you have many small objects, they might not be filled efficiently.
The -XX:NewRatio parameter, common in other collectors, is less direct with G1. Instead, G1 uses -XX:G1NewSizePercent and -XX:G1MaxNewSizePercent to control the initial and maximum percentage of the heap that can be occupied by young regions.
java -Xms4g -Xmx4g -XX:MaxGCPauseMillis=200 -XX:G1NewSizePercent=10 -XX:G1MaxNewSizePercent=30 -jar myapp.jar
This configuration suggests that G1 should start with at least 10% of the heap as young regions and can grow up to 30%. If your application creates objects very rapidly, you might need to increase G1MaxNewSizePercent to give G1 more space to collect young objects before they need to be promoted to the old generation.
The -XX:InitiatingHeapOccupancyPercent (IHOP) is critical for triggering concurrent marking cycles. This is the percentage of the entire heap that must be occupied before G1 starts its initial marking phase. The default is typically 45. If your application fills up the heap quickly, you might want to lower this.
java -Xms4g -Xmx4g -XX:MaxGCPauseMillis=200 -XX:InitiatingHeapOccupancyPercent=35 -jar myapp.jar
Lowering IHOP to 35 means G1 will start its work to clean the old generation sooner, potentially preventing a long pause when the heap eventually fills up.
If you’re seeing frequent "Humongous object allocation" errors, it means you’re allocating objects larger than half a G1 region. G1 has special handling for these, but it can be inefficient. You might need to increase G1HeapRegionSize or, more commonly, tune your application to avoid such large allocations.
The G1ConcRefinementThreads parameter controls how many threads are dedicated to handling changes in the old generation during the concurrent marking phase. If you have a very high rate of object allocation and mutation in the old generation, increasing this can help G1 keep up.
java -Xms4g -Xmx4g -XX:MaxGCPauseMillis=200 -XX:G1ConcRefinementThreads=4 -jar myapp.jar
Here, we’ve set it to 4 threads. The default is usually 1. Increasing this allows G1 to track changes more effectively without impacting application threads as much.
One subtlety is how G1 handles mixed collections (collecting both young and old regions). G1 tries to select regions for collection that will yield the most garbage. The -XX:G1MixedGCLiveThresholdPercent (default 85) influences when G1 considers a region in the old generation to be "mostly live" and thus less of a candidate for collection. Lowering this might make G1 more aggressive in collecting from the old generation.
The real magic of G1 lies in its adaptive nature, trying to balance throughput and pause times. When you tune these parameters, you’re not just setting static values; you’re nudging G1’s internal heuristics to better match your application’s specific allocation patterns and performance goals.
A common pitfall is focusing solely on MaxGCPauseMillis without considering the trade-off in throughput. If you aggressively lower MaxGCPauseMillis, G1 might have to do more frequent, smaller collections, which can increase overall CPU usage.
The next challenge you’ll likely encounter after tuning GC pauses is managing application memory leaks, which can still cause the heap to grow indefinitely, regardless of GC tuning.