Tuning G1 in production: the few options that matter

G1 is designed to need little tuning. Most production problems attributed to "G1 settings" are really a heap that is too small, a pause target that cannot be met, or flags copied from an older collector. This guide covers the handful of options that do matter, in the order to try them.
How G1 works, briefly
The G1 chapter of the tuning guide describes the design. The heap is divided into equal-sized regions. Each region is, at any time, eden, survivor, old or humongous. G1 collects young regions in stop-the-world young pauses, marks the old generation concurrently once occupancy crosses a threshold, and then reclaims the old regions with the most garbage in mixed pauses that collect young and some old regions together. A full collection is the fallback when all else fails.
To meet its pause-time goal, G1 chooses how many regions to put in the young generation and how many old regions to add to each mixed collection, based on what it has measured in earlier pauses.
Step 1: get the heap size right
Everything else depends on it. Set the maximum explicitly:
-Xmx6g -Xms6g
Setting -Xms equal to -Xmx removes heap resizing from the picture and makes behaviour more predictable; -XX:+AlwaysPreTouch additionally commits and touches all heap pages at startup, which the java reference notes is disabled by default. Pre-touching lengthens startup and makes the whole heap resident immediately, so use it only where memory is truly reserved for the process.
How big? Look at old-generation occupancy after mixed collections in the GC log: that is your live data set. The heap needs room above it for allocation between collections and for concurrent marking to finish. A heap with very little headroom above the live set turns into back-to-back collections and, eventually, full GCs.
Step 2: set a realistic pause target
-XX:MaxGCPauseMillis=100
The default target is 200 ms. It is a goal, not a guarantee. Lowering it makes G1 use a smaller young generation and smaller collection sets: pauses get shorter but more frequent, and total GC overhead rises. A target far below what the hardware and live set allow does not produce short pauses; it produces many pauses and, often, worse throughput. Lower it in steps and check both pause times and GC overhead after each change.
Do not fix the young generation size
The tuning guide advises against options such as -Xmn or -XX:NewRatio with G1: the young generation size is the main lever G1 uses to meet its pause target, and fixing it disables that lever. This is the most common leftover from Parallel or CMS configurations. Remove it first.
Step 3: concurrent marking start (IHOP)
G1 starts concurrent marking when old-generation occupancy reaches the initiating heap occupancy percent (IHOP). The java reference gives a default of 45% for -XX:InitiatingHeapOccupancyPercent, but by default G1 determines the threshold itself from earlier application behaviour (adaptive IHOP). The G1 tuning chapter notes that these predictions can be wrong when the application's behaviour changes.
The symptom that marking starts too late is a concurrent cycle that does not finish before the heap fills. The tuning chapter says full collections are typically preceded by collections that hit an evacuation failure, tagged (Evacuation Failure) in the log, and that full collections caused by high old-generation occupancy appear as Pause Full (G1 Compaction Pause). Options, in order:
- more heap headroom (step 1);
- a larger buffer in the adaptive calculation with
-XX:G1ReservePercent(default 10%), which makes marking start earlier; - switching adaptive IHOP off with
-XX:-G1UseAdaptiveIHOPand setting a fixed threshold, for example-XX:InitiatingHeapOccupancyPercent=35; - more concurrent marking threads with
-XX:ConcGCThreads, if spare CPU exists.
Step 4: humongous objects and region size
An object of at least half a region is humongous: it is allocated directly in one or more contiguous old regions. Large arrays (buffers, big collections, serialised payloads) are the usual examples. Many short-lived humongous objects waste space and can trigger extra collections.
The region size is chosen ergonomically from the heap size, with a goal of about 2048 regions according to the java reference. You can see it in the first lines of the GC log (Heap Region Size) and in jcmd <pid> GC.heap_info. If the log shows frequent humongous allocations (G1 Humongous Allocation as a GC cause), either reduce the size of those allocations in code or raise the region size so that they are no longer humongous:
-XX:G1HeapRegionSize=16m
The value must be a power of two. Larger regions mean fewer regions for a given heap, which reduces G1's flexibility, so change this only with evidence from the log.
Mixed collection options
A few options shape how old regions are reclaimed after marking. Their defaults, from the java reference: -XX:G1MixedGCCountTarget 8 mixed collections, -XX:G1HeapWastePercent 5%. They rarely need changes. If mixed pauses exceed the target, a higher count target spreads the work over more, smaller pauses. Leave them alone until the log shows a specific problem.
Threads
-XX:ParallelGCThreads sets the number of threads used during pauses and -XX:ConcGCThreads those used for concurrent work. Both default to values derived from the number of CPUs available to the JVM. In containers the JVM counts the CPUs of the container limit; if that detection is wrong, fix it with -XX:ActiveProcessorCount rather than setting thread counts by hand (see the JVM in containers).
Useful extras
-XX:+UseStringDeduplicationlets the collector share the backing arrays of equal strings. The java reference says it is disabled by default. It helps applications with many duplicate strings; measure, as it adds GC work.- Returning memory to the OS. Since JDK 12, JEP 346 lets G1 run periodic collections when idle to uncommit unused heap, controlled by
-XX:G1PeriodicGCInterval, an interval in milliseconds where zero, the default, disables the feature. -XX:+ExplicitGCInvokesConcurrentturnsSystem.gc()into a concurrent cycle instead of a full pause, a safer alternative to disabling explicit GC entirely.
A sensible starting configuration
java \
-Xms6g -Xmx6g \
-XX:MaxGCPauseMillis=150 \
-Xlog:gc*:file=/var/log/app/gc.log:time,uptime,level,tags:filecount=5,filesize=20m \
-XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/var/dumps/ \
-jar app.jar
The heap size and pause target are examples to adapt; the logging and dump options are the part worth keeping everywhere. Change one option at a time, and keep the GC log of each run.
Sources
- Garbage-First (G1) Garbage Collector and G1 tuning, HotSpot GC Tuning Guide
- The java command (G1 options and defaults)
- JEP 346: Promptly Return Unused Committed Memory from G1
Garbage collectionChoosing a garbage collector
Latency, throughput or footprint: what each collector trades, and why your container may be running Serial.
Garbage collectionReading GC logs with -Xlog
One line of configuration, the legacy flag mapping, and how to read pause lines for leaks and undersized heaps.
ProductionThe JVM in containers
How limits are detected, what MaxRAMPercentage really sets, and a budget that keeps the OOM killer away.