Garbage collection

Tuning G1 in production: the few options that matter

Garbage collection · Published 2026-10-22

Close-up of a chrome pressure gauge with a needle on a white dial
G1 adjusts itself against a target; most tuning is choosing the target well. Photo: rawpixel, CC0.

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:

  1. more heap headroom (step 1);
  2. a larger buffer in the adaptive calculation with -XX:G1ReservePercent (default 10%), which makes marking start earlier;
  3. switching adaptive IHOP off with -XX:-G1UseAdaptiveIHOP and setting a fixed threshold, for example -XX:InitiatingHeapOccupancyPercent=35;
  4. 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

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