CodeOverflow

892
votes

How to optimize Java application performance with JVM tuning?

Our Java microservice is experiencing performance issues under high load. We're running Spring Boot on Java 17 with the following setup:

  • 8GB RAM allocated to the JVM
  • Average request rate: 10,000 req/s during peak
  • GC pauses causing latency spikes (p99 > 500ms)
  • High CPU usage during garbage collection

Current JVM flags:

java -Xms4g -Xmx8g -jar application.jar

What are the best practices for JVM tuning to reduce GC pauses and improve throughput? Should I switch to G1GC or try ZGC?

7 Answers

1247
votes

Excellent question! JVM tuning can dramatically improve performance. Here's a comprehensive guide:

1. Choose the Right Garbage Collector

For Low Latency (Your Use Case):

# ZGC - Best for sub-10ms pause times
java -Xms8g -Xmx8g \
     -XX:+UseZGC \
     -XX:ZCollectionInterval=5 \
     -XX:ZAllocationSpikeTolerance=2 \
     -jar application.jar

# G1GC - Good balance between throughput and latency
java -Xms8g -Xmx8g \
     -XX:+UseG1GC \
     -XX:MaxGCPauseMillis=200 \
     -XX:G1HeapRegionSize=16m \
     -XX:InitiatingHeapOccupancyPercent=45 \
     -XX:G1ReservePercent=10 \
     -jar application.jar

2. Essential JVM Flags for Production

java -Xms8g -Xmx8g \                           # Set min=max heap
     -XX:+UseG1GC \                             # Use G1 collector
     -XX:MaxGCPauseMillis=200 \                 # Target pause time
     -XX:+UseStringDeduplication \              # Reduce string memory
     -XX:+ParallelRefProcEnabled \              # Parallel reference processing
     -XX:MaxMetaspaceSize=512m \                # Limit metaspace
     -XX:+HeapDumpOnOutOfMemoryError \          # Debug OOM issues
     -XX:HeapDumpPath=/var/log/heapdump.hprof \ # Dump location
     -XX:+UseCompressedOops \                   # Compress object pointers
     -XX:+UseCompressedClassPointers \          # Compress class pointers
     -XX:ReservedCodeCacheSize=256m \           # For JIT compiled code
     -XX:+UseTLAB \                             # Thread-local allocation
     -jar application.jar

3. GC Logging for Analysis

# Enable detailed GC logging
-Xlog:gc*:file=/var/log/gc.log:time,uptime,level,tags:filecount=5,filesize=100M

# Or for older Java versions
-Xloggc:/var/log/gc.log \
-XX:+PrintGCDetails \
-XX:+PrintGCDateStamps \
-XX:+UseGCLogFileRotation \
-XX:NumberOfGCLogFiles=5 \
-XX:GCLogFileSize=100M

4. Application-Level Optimizations

// Use object pooling for frequently created objects
ObjectPool stringBuilderPool = new GenericObjectPool<>(
    new StringBuilderFactory()
);

// Optimize collection sizes
Map map = new HashMap<>(expectedSize, 0.75f);

// Use primitive collections when possible
IntArrayList ids = new IntArrayList(1000);

// Lazy initialization for expensive objects
private volatile ExpensiveObject instance;

public ExpensiveObject getInstance() {
    if (instance == null) {
        synchronized (this) {
            if (instance == null) {
                instance = new ExpensiveObject();
            }
        }
    }
    return instance;
}

5. Monitoring and Profiling

// Add JMX monitoring
-Dcom.sun.management.jmxremote \
-Dcom.sun.management.jmxremote.port=9010 \
-Dcom.sun.management.jmxremote.authenticate=false \
-Dcom.sun.management.jmxremote.ssl=false

// Enable Flight Recorder
-XX:StartFlightRecording=duration=60s,filename=/var/log/recording.jfr

6. Spring Boot Specific Optimizations

# application.properties
spring.jmx.enabled=false  # Disable if not needed
server.tomcat.threads.max=200
server.tomcat.threads.min-spare=50
server.tomcat.accept-count=100
server.tomcat.max-connections=8192

# Connection pool tuning
spring.datasource.hikari.maximum-pool-size=20
spring.datasource.hikari.minimum-idle=10
spring.datasource.hikari.connection-timeout=30000
spring.datasource.hikari.idle-timeout=600000
spring.datasource.hikari.max-lifetime=1800000

Performance Testing Results

After applying these optimizations to a similar system:

  • P99 latency: 500ms → 45ms (91% improvement)
  • Throughput: 10k req/s → 18k req/s (80% improvement)
  • GC pause time: 200ms avg → 15ms avg (92.5% improvement)
  • CPU usage during GC: 40% → 8%

Pro tip: Always benchmark with realistic load using tools like JMH, Gatling, or Apache Bench!

342
votes

Great comprehensive answer! I'd add that ZGC is a game-changer for Java 17+. We migrated to ZGC and saw amazing results:

# Our production ZGC config
-Xms16g -Xmx16g \
-XX:+UseZGC \
-XX:+ZGenerational \          # Java 21+ only
-XX:SoftMaxHeapSize=12g \     # Soft limit for better responsiveness
-XX:ZCollectionInterval=5 \
-XX:ZUncommitDelay=300

Results:

  • 99.99th percentile GC pause: 0.8ms (yes, sub-millisecond!)
  • Average pause time: 0.2ms
  • No more stop-the-world pauses affecting user requests

The only caveat is ZGC uses slightly more CPU (~5-10% overhead), but the latency improvements are absolutely worth it for user-facing applications!

156
votes

Don't forget to profile before optimizing! I use async-profiler for production profiling:

# CPU profiling
./profiler.sh -d 60 -f /tmp/profile.html [PID]

# Allocation profiling
./profiler.sh -d 60 -e alloc -f /tmp/alloc.html [PID]

# Lock profiling
./profiler.sh -d 60 -e lock -f /tmp/locks.html [PID]

This helped us identify that 60% of our GC pressure came from excessive String concatenation in logging. After fixing that alone, we reduced GC pauses by 40%!