Garbage Collection (GC) is a cornerstone of the Java ecosystem, providing automatic memory management. While it works seamlessly most of the time, performance-critical applications often suffer from symptoms like high latency, unpredictable pauses, or excessive CPU usage. This is where GC tuning becomes essential. The goal is not to eliminate GC, but to configure it to work efficiently for your specific application's needs.
1. The Golden Rule: Measure First, Tune Later
Never tune GC based on a hunch. Always use data. The first step is to enable GC logging, which provides a detailed record of every GC event.
Command to Enable GC Logging (Java 8 & 11+):
# Java 8 Style java -Xloggc:gc.log -XX:+PrintGCDetails -XX:+PrintGCDateStamps -XX:+UseGCLogFileRotation -XX:NumberOfGCLogs=5 -XX:GCLogFileSize=10M -jar myapp.jar # Java 9+ Style (Unified Logging) java -Xlog:gc*,gc+age=trace,gc+heap=debug:file=gc.log:tags,uptime,level:filecount=5,filesize=10M -jar myapp.jar
Key Tools for Analysis:
- GC Logs: Analyze with tools like GCeasy, G1GC Plotter, or HPjmeter.
- JVM Tools: Use
jstat,jconsole, orVisualVMfor real-time monitoring. - APM Tools: Tools like Dynatrace, AppDynamics, or New Relic can correlate GC behavior with application performance.
2. Know Your Garbage Collectors
Choosing the right collector is the most critical tuning decision.
| Collector | JVM Argument | Best For | Focus |
|---|---|---|---|
| G1 GC (Default) | -XX:+UseG1GC | Most modern applications. Balanced throughput and low latency. Heaps from 6GB to many tens of GB. | Manages heap in regions. Aims to meet pause time goals. |
| Parallel GC | -XX:+UseParallelGC | Batch processing, high throughput. Where peak performance is key and pauses of 1+ sec are acceptable. | Maximizes application throughput over pause time. |
| ZGC | -XX:+UseZGC | Low-latency applications. Heaps from 8MB to 16TB+. Requires latest JDK (13+ for production). | Sub-millisecond max pause times, regardless of heap size. |
| Shenandoah | -XX:+UseShenandoahGC | Low-latency applications. Similar goals to ZGC, different implementation. | Concurrent compaction with very short pause times. |
Recommendation: Start with G1GC. It's the default for a reason and performs well for most workloads.
3. Common Tuning Parameters & Strategies
Here’s how to tune the most common collectors.
Tuning G1 Garbage Collector
G1GC is designed to meet a pause time goal.
- Set a Pause Time Goal:
-XX:MaxGCPauseMillis=200(e.g., 200 milliseconds)- This is a goal, not a promise. The JVM will try to achieve it, but it's not a hard guarantee.
- Set the Heap Size (Properly):
- Do not set
-Xmsand-Xmxto wildly different values. Setting them equal (-Xms4g -Xmx4g) prevents heap resizing pauses and improves performance. - Let the JVM use the heap. The goal is to have 30-40% free space after a Full GC. If you see frequent Full GCs, your heap is likely too small.
- Do not set
- Control Parallelism:
-XX:ParallelGCThreads=N: Sets the number of threads for stop-the-world phases. Usually defaults well (number of CPUs).
- Control Concurrency:
-XX:ConcGCThreads=N: Sets the number of threads for concurrent phases (like marking). If the concurrent phase can't keep up, you may need to increase this.
Tuning Parallel GC
The goal is throughput. It's tuned by controlling the proportion of time spent in GC.
- Set Throughput Goal:
-XX:GCTimeRatio=99(Default is 99). This means the application should run for 99 times longer than the GC. The formula is1 / (1 + GCTimeRatio). So, 99 means ~1% of time is spent in GC.
- Maximum Pause Time Fallback:
-XX:MaxGCPauseMillis=N: The collector will try to keep pauses below this, but will prioritize the throughput goal (GCTimeRatio).
4. A Step-by-Step Tuning Workflow
- Define Your Goal: What are you optimizing for? Lower latency? Higher throughput? Predictable pauses?
- Collect Baseline Data: Run your application under load with default settings and enable GC logging.
- Analyze the Logs:
- Are Young Gen collections too frequent? -> Increase
-Xmn(Young Gen size). - Are Young Gen pauses too long? -> Decrease
-Xmn. - Are there Full GCs? -> This is a red flag! Your heap is likely too small, or there is a memory leak.
- Is the Old Gen constantly growing? -> Check for memory leaks. You might need to adjust the
-XX:InitiatingHeapOccupancyPercent(for G1) to start concurrent cycles earlier.
- Are Young Gen collections too frequent? -> Increase
- Apply One Change at a Time: Change one parameter, test, and measure. Changing multiple things at once makes it impossible to know what worked.
- Iterate: Repeat the cycle of change -> test -> measure until your performance goals are met.
5. Common "Anti-Patterns" to Avoid
- Setting an Aggressive
MaxGCPauseMillis: Setting this too low (e.g., 20ms on a 32GB heap) will force the GC to run much more frequently, killing throughput and increasing overall latency. - Over-sizing the Young Generation: A massive Young Gen leads to fewer, but much longer, Young GC pauses.
- Under-sizing the Heap: This leads to constant garbage collection and frequent, disastrous Full GCs.
- Using the Serial Collector for Server Applications: The Serial GC (
-XX:+UseSerialGC) is for tiny applications (e.g., a few hundred MB heap). Never use it for a server-side app.
Conclusion: Tuning is a Journey
There is no single "best" configuration. The optimal setup depends entirely on your application's object allocation patterns, heap size, and performance requirements. By following a disciplined, data-driven approach—enabling logging, analyzing results, and making incremental changes—you can transform Garbage Collection from a source of performance anxiety into a well-oiled component of your high-performance Java application.
Start with the defaults, measure, and only tune when the data tells you to.