As we all know, when a Java application is up and running, garbage collection can sometimes cause the application to slow down, or experience unexpected pauses. This happens with all kinds of applications: REST APIs, a small service that responds to a message, or a big system that processes huge amounts of data at a time. What this means is that Java garbage collection is often the deciding factor as to how well a continuously-running Java application performs.
Starting at Java 9, the JVM uses G1GC as the default way to collect garbage. G1GC replaced these two algorithms – CMS and Parallel GC. The new algorithm is better at handling a lot of data, and tries to keep things running smoothly without stopping for long. The old CMS had issues like data getting fragmented, causing the system to fail without warning. On the other hand, the Parallel GC was fast, but it would pause the application for longer.
G1GC is essentially designed to balance the strengths of CMS and Parallel GC. It keeps pause times shorter while still maintaining good overall throughput. This balance is why G1GC became the default garbage collector starting in Java 9, although Parallel GC can still be a good option for throughput-focused workloads such as batch processing.
This guide is for Java developers, engineers who focus on performance, and architects who want to know more than the basics about Java Garbage Collection and G1GC. You will learn how G1GC works, which JVM settings are important, and how to adjust them to suit the workloads your application runs in production.
Tip: G1GC works well out of the box. But a poorly tuned G1GC can be worse than ParallelGC. The default settings are a starting point, not a finish line.
Understanding G1GC Internals (Quick Recap)
Before diving into tuning, it’s worth revisiting how G1GC is architected, because most tuning decisions only make sense in that context.
Region-Based Heap Architecture
The Region-Based Heap Architecture is different from the earlier way of doing things. Traditionally, the heap is divided into fixed parts: the young generation at the bottom of the heap and the old generation at the top.
G1GC does it differently. It divides the heap into lots of parts called regions. These regions are all the same size. They are usually between 1MB and 32MB each. The goal is to have around 2,048 regions in total.
Each region is dynamically assigned a role:
- Eden: Where new objects are allocated
- Survivor: Objects that survived at least one GC cycle
- Old: Long-lived objects promoted from the young generation
- Humongous: Objects larger than 50% of a region size, allocated directly in old generation.
The “Garbage First” Principle
G1GC gets its name from its collection strategy: it prioritizes regions with the most reclaimable garbage first. The G1GC keeps an eye on how much good stuff’s in each area. Then it picks which areas to clean up based on how much time it has to do the job. The goal of the G1GC is to free up as much space as possible in the time allowed, which is usually just a few milliseconds. The G1GC does this by focusing on the areas with the most garbage first.
GC Phases
G1GC operates through a series of phases:
- Young GC: Collects Eden and Survivor regions. Always stop-the-world (STW).
- Concurrent Marking: Scans the heap concurrently with your application to identify live objects in Old regions.
- Mixed GC: Collects both Young regions and a selected set of Old regions that have the highest garbage ratio.
- Full GC (last resort): A single-threaded or parallel stop-the-world collection of the entire heap. This is what you want to avoid.
Note: STW pauses freeze all application threads. Concurrent phases run alongside your application. They’re cheaper, but must complete before space runs out; otherwise the JVM may trigger a Full GC or even an OutOfMemoryError, leading to long pauses or application failure.
Key JVM Flags & Their Defaults
The following table summarizes the most important G1GC tuning flags. Understanding these is the foundation of any tuning effort.
| Flag | Default | Purpose |
| -XX:MaxGCPauseMillis | 200ms | Soft pause-time target. G1 aims for this, but cannot guarantee it |
| -XX:G1HeapRegionSize | Auto (1MB–32MB) | Controls region size; auto-calculated to target ~2048 regions |
| -XX:InitiatingHeapOccupancyPercent | 45% | Heap occupancy % at which concurrent marking starts |
| -XX:ParallelGCThreads | Based on CPU count | Number of STW GC worker threads |
| -XX:ConcGCThreads | ~1/4 of ParallelGCThreads | Threads for concurrent marking phase |
| -XX:G1NewSizePercent | 5% | Minimum young gen size as % of heap |
| -XX:G1MaxNewSizePercent | 60% | Maximum young gen size as % of heap |
| -XX:GCTimeRatio | 12 | Max fraction of time in GC (~7.6% GC overhead target) |
| -XX:G1ReservePercent | 10% | Reserve heap space to reduce evacuation failures |
| -XX:G1MixedGCCountTarget | 8 | Target number of Mixed GC cycles per marking cycle |
Core Tuning Strategies
Core tuning strategies focus on configuring key G1GC parameters to balance pause times, throughput, and heap stability under real production workloads. Here’s a few commonly-used strategies:
Setting the Right Pause-Time Goal (-XX:MaxGCPauseMillis)
The -XX:MaxGCPauseMillis flag is really important for G1GC. It helps the collector figure out how long it should ideally pause. But here is the thing: this is not a fixed limit; it is like a goal. G1GC will try its best to keep pauses short. Sometimes it just cannot do that. If the heap is completely full or there is too much live data the pauses are going to be longer than the target.
The target pause time affects how G1GC decides the size of the young generation. If you want short pauses, G1GC will make the young generation smaller. This means there is a trade-off between the -XX:MaxGCPauseMillis flag and the young generation size.
| Pause Target | Young Gen Size | GC Frequency | Throughput |
| 200ms (default) | Medium | Moderate | Good |
| 50–100ms (low latency) | Smaller | More frequent | Reduced |
| 500ms+ (high throughput) | Larger | Less frequent | Higher |
💡 Tip: Start with the default 200ms and benchmark your application under a realistic load before tightening the target. Premature optimization here often hurts throughput without meaningful latency benefit.
Here are our recommendations to start the configuration:
-XX:+UseG1GC-XX:MaxGCPauseMillis=200-Xms4g -Xmx4g-Xlog:gc*:file=gc.log:time,uptime:filecount=5,filesize=20m
Heap Sizing: Don’t Over-Constrain G1
One of the most common mistakes when configuring G1GC, especially for teams migrating from CMS or Parallel GC, is explicitly setting the young generation size using -Xmn or -XX:NewRatio. With G1GC, this is counterproductive.
G1GC dynamically resizes the young generation at every GC cycle to meet the pause target. When you lock the young generation to a fixed size, you remove G1’s primary control lever, it can no longer shrink or grow the young generation to hit your pause goal. The result is often worse performance than if you had said nothing.
💡 Tip: Never set -Xmn or -XX:NewRatio with G1GC. Let G1 manage young generation sizing automatically within the bounds set by -XX:G1NewSizePercent and -XX:G1MaxNewSizePercent.
Instead, focus on these two settings:
- Set -Xms = -Xmx: Prevents heap resize pauses caused by the JVM growing the heap on demand. Pre-allocate the full heap at startup.
- Give G1 enough heap: G1 needs room to work. Starving it of heap leads to more frequent GC and eventual Full GC.
Tuning InitiatingHeapOccupancyPercent (IHOP)
The -XX:InitiatingHeapOccupancyPercent (IHOP) flag controls when G1GC kicks off its concurrent marking cycle. By default, it triggers when the heap is 45% full. The goal is to complete marking before the old generation fills up. If marking finishes too late, G1 falls back to a Full GC.
The right IHOP setting depends on your allocation rate:
- High allocation rate apps: Objects flood into old gen quickly. Lower IHOP (e.g., 30–35%) to start marking sooner.
- Low allocation rate apps: Default 45% or higher is fine. Starting marking too early wastes CPU on concurrent threads.
Tip: Java 9+ introduced Adaptive IHOP. G1 automatically learns from previous GC cycles and adjusts the threshold dynamically. In most cases, leave it adaptive (enabled by default) and only intervene if you see consistent Full GC due to late marking completion.
If you need to tune manually, disable adaptive IHOP and set it explicitly:
-XX:-G1UseAdaptiveIHOP-XX:InitiatingHeapOccupancyPercent=35 # Lower for high allocation apps
Avoiding and Diagnosing Full GC
A Full GC in G1GC is a last-resort event. It means G1 ran out of space or time to do its job incrementally. It stops all application threads and performs a complete heap collection, often resulting in pauses 10–100x longer than a normal young GC. In production, a single Full GC can cascade into request timeouts, SLA violations, and cascading failures.
Common Causes
- Evacuation Failure / To-Space Exhausted: G1 couldn’t find enough free regions to move live objects during a GC. The heap is effectively too small.
- Concurrent Marking Didn’t Finish in Time: The application allocated objects faster than concurrent marking could complete, forcing a Full GC.
- Humongous Object Fragmentation: Large objects can’t find contiguous free regions, triggering a compacting Full GC.
Solution
| Root Cause | Fix |
| Heap too small | Increase -Xmx |
| Marking starts too late | Lower -XX:InitiatingHeapOccupancyPercent |
| Concurrent marking too slow | Increase -XX:ConcGCThreads |
| Humongous fragmentation | Increase -XX:G1HeapRegionSize |
| Reserve too small | Increase -XX:G1ReservePercent to 15–20% |
Humongous Object Handling
In G1GC, any object larger than 50% of the region size is classified as a humongous object. These objects are special-cased: they bypass the young generation entirely and are allocated directly in contiguous old generation regions called humongous regions.
This creates several problems:
- Humongous objects are only reclaimed during concurrent marking cycles or Full GC, not during regular young GCs
- They require contiguous free regions, making fragmentation more likely
- Pre-JDK 8u40, humongous objects were never reclaimed until Full GC, a known performance issue
Detecting Humongous Allocations
Enable heap detail logging
-Xlog:gc+heap=info
Look in GC Logs for:
[gc,heap] Humongous regions: 12 -> 14[gc,alloc] G1 Humongous Allocation (region size 4096K, allocation size 3200K)
Solutions
- Increase -XX:G1HeapRegionSize (e.g., from 4MB to 8MB) so more objects fall below the humongous threshold
- Audit your code for large byte arrays, large collections, or large cached objects. Reduce their size or split them
- For JSON-heavy apps, consider streaming parsers instead of loading entire payloads into memory
Tips: Region sizes must be a power of 2 between 1MB and 32MB. Valid values: 1, 2, 4, 8, 16, 32 MB. Set with: -XX:G1HeapRegionSize=8m
String Deduplication
If your application is heavy on String usage (think REST APIs, JSON processing, log aggregation, or XML parsing), you can reduce heap pressure by enabling String Deduplication, introduced in Java 8u20.
The JVM maintains a deduplication table and identifies duplicate String objects (same character content, different heap addresses). When found, it replaces the duplicate’s backing char[] with a reference to the canonical copy, freeing the extra memory.
-XX:+UseStringDeduplication-XX:+PrintStringDeduplicationStatistics # (optional) log deduplication activity
Tips: String deduplication runs on GC threads and incurs a small CPU overhead. Enable it when heap pressure from Strings is confirmed via heap profiling, not by default on every application.
Cleaning Up Legacy GC Arguments
When migrating from CMS (-XX:+UseConcMarkSweepGC) or Parallel GC (-XX:+UseParallelGC), teams often carry over their old JVM arguments. With G1GC, these are either silently ignored or, worse, actively harmful.
Flags to remove when switching to G1:
| Remove this flag | Here’s why |
| -XX:+UseConcMarkSweepGC | Enables CMS, incompatible with G1 |
| -XX:CMSInitiatingOccupancyFraction | CMS-specific, no effect on G1 |
| -XX:+CMSParallelRemarkEnabled | CMS-specific, no effect on G1 |
| -XX:+UseParNewGC | Old young gen collector, deprecated/removed in Java 9+ |
| -XX:NewRatio | Overrides G1’s dynamic young gen sizing |
| -Xmn | Same problem, defeats adaptive young gen |
| -XX:MaxPermSize | PermGen removed in Java 8, invalid flag |
Enabling & Reading G1 GC Logs
GC logs are the single most valuable diagnostic tool for G1GC tuning. No performance investigation should begin without them.
Enabling GC Logging
For Java 9+ (Unified Logging)
-Xlog:gc*:file=gc.log:time,uptime:filecount=5,filesize=20m
For Java 8
-XX:+PrintGCDetails -XX:+PrintGCDateStamps -Xloggc:gc.log-XX:+UseGCLogFileRotation -XX:NumberOfGCLogFiles=5 -XX:GCLogFileSize=20m
For Minimal debug info (Java 9+)
-Xlog:gc=info:file=gc.log:time
Key Log Events to Watch
Here are the critical event types you’ll encounter in G1 GC logs:
| Log Event | What does it mean? | Should we be concerned? |
| Pause Young (Normal) | Routine young generation collection | Normal |
| Pause Young (Concurrent Start) | Triggers start of concurrent marking cycle | Watch |
| Pause Young (Prepare Mixed) | Preparing regions for upcoming Mixed GC | Normal |
| Pause Mixed | Collecting young + selected old gen regions | Normal |
| Pause Remark | STW phase to finish concurrent marking | Monitor duration |
| Pause Cleanup Short | STW to update region statistics | Normal |
| Pause Full (Allocation Failure) | Full GC, heap exhausted | Critical, investigate |
| To-space exhausted | G1 ran out of free regions during evacuation | Critical, pre-Full GC warning |
Sample Log Excerpt Analysis
Let’s walk through a real G1 GC log entry:
[2025-03-01T10:45:22.341+0000][5.234s][info][gc] GC(17) Pause Young (Normal) (G1 Evacuation Pause) 512M->204M(2048M) 35.234ms# Breaking this down:# [timestamp] = Wall-clock time of GC event# GC(17) = 18th GC event (0-indexed)# Pause Young (Normal)= Event type# 512M->204M(2048M) = Heap before -> after (max heap)# 35.234ms = Total STW pause time (real-world)
GC Analysis Tools
Manually reading thousands of GC events is impractical. These tools automate the analysis:
- GCeasy: Upload your GC log and get automated analysis, pause time trends, throughput metrics, memory pool graphs, and specific recommendations. Free tier available.
- GCViewer: Open-source desktop tool for visual GC log analysis.
- Java Flight Recorder (JFR): Built into the JDK, provides deep JVM-level diagnostics including GC, thread, and I/O profiling. Zero-overhead when enabled correctly.
- JDK Mission Control (JMC): GUI for analyzing JFR recordings.
Tuning for Specific Workloads
- High-Throughput Applications (Batch Jobs, ETL): The priority is minimizing total GC overhead, not individual pause time. Allowing slightly longer pauses can reduce GC frequency and improve overall processing efficiency. Monitor GC overhead and keep it below ~10% of total runtime.
- Low-Latency Applications (APIs, Microservices): The focus is predictable and short GC pauses. Smaller young generations help reduce pause duration, though they may slightly reduce throughput. Always monitor P99 and P99.9 latency, not just average response time.
- Large Heap Applications (>32GB): G1GC performs particularly well with large heaps, where region-based collection improves scalability. The main tuning focus is ensuring concurrent marking keeps up with the allocation rate. Proper region sizing and early marking help maintain stable GC behavior in large-memory environments.
Common G1GC Problems & Solutions (Troubleshooting Table)
Here’s a quick comparison table of the common G1GC problems and solutions:
| Problem | Symptom in GC Logs | Solution |
| Frequent Full GC | Pause Full (Allocation Failure) | Increase -Xmx; lower IHOP; increase ConcGCThreads |
| Long Young GC pauses | Pause Young > MaxGCPauseMillis target | Lower MaxGCPauseMillis; check allocation rate; profile allocations |
| Humongous allocations | Humongous regions growing; humongous alloc events | Increase G1HeapRegionSize; reduce object sizes in code |
| Evacuation failure | To-space exhausted; Evacuation Failed | Increase heap; increase G1ReservePercent to 15–20% |
| High GC overhead | GCTimeRatio threshold exceeded | Tune heap size; reduce allocation rate; profile hot paths |
| Concurrent marking late | Concurrent cycle starts too close to Full GC | Lower InitiatingHeapOccupancyPercent |
| Long Remark pauses | Pause Remark > 100ms | Reduce object reference churn; investigate large reference queues |
| Survivor overflow | Objects promoted directly to old gen | Tune -XX:MaxTenuringThreshold; increase survivor space |
G1GC vs ZGC vs Shenandoah: When to Move On
G1GC is the best default for most production Java workloads, but it’s not the right choice for every application. Understanding where its limits are is just as important as knowing how to tune it.
| Collector | Best For | Typical Pause | Heap Size | Production Since |
| G1GC | Most production workloads; balanced throughput & latency | 10–200ms | 4GB–256GB | Java 9 (default) |
| ZGC | Ultra-low latency; sub-millisecond goals | <1ms (Java 16+) | 8MB–16TB | Java 15 (production) |
| Shenandoah | Low-latency; consistent short pauses | <10ms typical | Any | Java 15 (production) |
The key rule of thumb:
- Pause targets > 50ms: G1GC is likely sufficient with proper tuning.
- Pause targets 10–50ms: G1GC may work, but benchmark carefully. Consider ZGC/Shenandoah.
- Pause targets < 10ms: Move to ZGC (Java 15+) or Shenandoah. G1GC cannot reliably hit sub-10ms goals.
If you’re deciding between these two low-latency collectors, see our detailed comparison: ZGC vs Shenandoah: Which Java GC Should You Choose?
Conclusion
G1GC is one of the most sophisticated and adaptive garbage collectors ever built into a production runtime, but adaptive doesn’t mean zero-effort. The default configuration handles many workloads well, yet production-grade performance consistently requires deliberate tuning.
The key takeaways from this guide:
- Set a realistic pause goal: Start with 200ms and benchmark before tightening. Don’t guess.
- Never fix young gen size: Let G1 manage it dynamically. Avoid -Xmn and -XX:NewRatio.
- Match heap to workload: Set -Xms = -Xmx. Give G1 room to work without heap resize pauses.
- Watch for Full GC: It’s always a symptom of a deeper problem, heap sizing, IHOP, or humongous objects.
- Use GC logs: Enable GC logging on every production JVM. Analyze with GCeasy or JFR.
Know your limits: If you need sub-10ms pauses, G1 isn’t the right tool. Consider ZGC or Shenandoah.

Share your Thoughts!