Azul Statement on the eMudhra Intermediate CA Certificates Compromise
java-performance-bg

JVM Performance Optimization: A Practical Guide

Smart Summary

JVM performance optimization works best when you first identify which of four problem categories you face: garbage collection pressure, JIT compilation inefficiency, thread contention, or memory footprint. This practical guide gives targeted tuning approaches for each, a table of problem areas with standard and Azul Prime alternatives, and a step-by-step optimization sequence. It also explains when standard tuning is enough and when a JVM-level solution such as Azul Prime is worth evaluating.

JVM performance optimization is one of the most rewarding and most misunderstood areas of Java engineering. It is rewarding because the JVM exposes a rich set of tunable parameters and observable metrics that make performance problems diagnosable and fixable. It is misunderstood because many engineers optimize the wrong things, apply tuning changes without measurement, or fail to address the root causes of performance problems.

The Four Primary JVM Performance Problem Categories

Before tuning anything, identify which category of problem you are dealing with:

  • Garbage Collection pressure: high allocation rates, long GC pauses, frequent full GCs, memory leaks
  • JIT compilation inefficiency: slow warmup, deoptimization spikes, inconsistent throughput across JVM instances
  • Thread contention: high lock wait times, thread starvation, CPU saturation without proportional throughput
  • Memory footprint: excessive heap usage, metaspace growth, off-heap memory leaks, native memory pressure

Most production performance incidents are caused by one or two of these simultaneously. Profiling with JFR or async-profiler before making any changes is essential. Tuning without data often makes things worse.

GC Performance Optimization

Understanding Your GC Behavior

The starting point is enabling GC logging in Java 9 and later: -Xlog:gc*:file=gc.log:time,uptime,level,tags:filecount=10,filesize=50m

Analyze the logs to identify patterns:

  • Minor GC pauses occurring more than several times per second: allocation rate too high or eden space too small
  • Promotion failures: old generation filling faster than it can be collected
  • Humongous object allocations in G1GC: large objects bypass normal eden allocation and cause fragmentation
  • Full GC events: worst case, indicates old generation pressure or metaspace exhaustion

Heap Sizing

Correct heap sizing is the most impactful GC tuning action:

  • Set -Xms equal to -Xmx to prevent heap resizing, which causes pauses and unpredictable GC behavior
  • Target 70-80% heap occupancy after a full GC. If consistently higher, the heap is too small
  • Allow at least 2x the live data set size for G1GC to maintain adequate collection headroom
  • For heaps above 32 GB, verify compressed object pointer viability given metaspace overhead

G1GC Tuning

G1GC is the default collector in Java 9 and later and the right choice for most general workloads. Key tuning parameters:

  • -XX:MaxGCPauseMillis: target pause goal (default 200ms). Lowering causes more frequent, shorter collections
  • -XX:G1HeapRegionSize: region size from 1 to 32 MB. Larger regions reduce humongous allocation frequency
  • -XX:G1NewSizePercent and -XX:G1MaxNewSizePercent: young generation size bounds
  • -XX:ConcGCThreads: concurrent marking threads. Increase if GC cannot keep pace with allocation rate
  • -XX:InitiatingHeapOccupancyPercent: when to start concurrent marking (default 45%). Lower to start earlier under sustained pressure

Important caution: avoid over-tuning G1GC. Each parameter has second-order effects, and improvements under one load pattern often cause regressions under another. Measure with production-representative load, not synthetic benchmarks.

ZGC and Shenandoah for Latency-Sensitive Workloads

If G1GC pauses consistently exceed your latency SLA after reasonable tuning, consider ZGC (-XX:+UseZGC) or Shenandoah (-XX:+UseShenandoahGC). Both are concurrent collectors targeting sub-10ms pause times at the cost of additional CPU for concurrent work.

Both collectors require adequate heap headroom to complete concurrent collection before allocation pressure forces a stop-the-world fallback. Sizing the heap too tightly defeats concurrent collection. For near-zero-pause requirements at any heap size, see the Azul Prime section below.

JIT Compilation Optimization

Understanding Tiered Compilation

HotSpot uses a tiered compilation model: Tier 0 (interpreter) through Tier 4 (C2 compiler, maximum optimization). Methods advance through tiers based on invocation and loop iteration counts. The compilation queue can become a bottleneck under high load.

Monitor JIT activity with:

  • jstat -compiler: shows compilation counts and failed compilations
  • JFR compilation events: method compilation times and deoptimization events
  • -XX:+PrintCompilation for targeted short-duration investigation (high output volume in steady state)

Effective JIT Tuning Parameters

  • -XX:CICompilerCount: number of JIT compiler threads. Increase on systems with many CPU cores and high compilation demand
  • -XX:ReservedCodeCacheSize: code cache size limit (default 240 MB in recent JDKs)d. If filled, JVM stops compiling and methods revert to interpretation
  • -XX:MaxInlineSize and -XX:FreqInlineSize: inlining thresholds. Increasing can help workloads with many small frequently-called methods

Warmup and Startup Optimization

Options for reducing warmup latency with standard OpenJDK:

  • Application-level warmup: send synthetic traffic before receiving real traffic. Effective but operationally complex
  • CRaC (Coordinated Restore at Checkpoint): snapshot a warm JVM state. Useful for serverless but requires careful handling of open connections and file descriptors
  • GraalVM Native Image: AOT compilation eliminates warmup but removes many Java features and requires upfront configuration for reflection and dynamic proxies
  • AppCDS (Application Class Data Sharing): pre-computes class metadata. Helps with class loading time but not JIT compilation warmup

ReadyNow! in Azul Prime avoids these trade-offs by persisting actual JIT compilation profiles across restarts, delivering warm performance from startup without AOT restrictions or CRaC complexity.

Thread Contention and Concurrency Optimization

Identifying Contention

Thread contention manifests as high CPU utilization without proportional throughput. JFR captures monitor contention events. jstack thread dumps reveal threads in BLOCKED state.

Common contention sources and remedies:

  • Synchronized collections: replace HashMap plus synchronized blocks with ConcurrentHashMap for concurrent access patterns
  • Overuse of synchronized: Java intrinsic locks have poor scalability under high contention. Consider ReentrantLock with tryLock for lock-free fast paths
  • Lock striping: partition shared data structures to reduce contention probability. ConcurrentHashMap uses this pattern internally
  • Thread pool misconfiguration: too few threads cause request queuing, too many cause context switching overhead. Profile actual utilization before resizing

Concurrency Patterns for High Throughput

  • Lock-free data structures: java.util.concurrent classes use compare-and-swap for non-blocking thread safety
  • Executor services: prefer structured concurrency using ExecutorService and CompletableFuture over raw thread management
  • Virtual threads in Java 21 and later: dramatically increase concurrent blocking operations per JVM instance, particularly valuable for I/O-bound workloads
  • Immutable objects and message-passing: the most effective concurrency optimization is often eliminating the need for synchronization entirely

Memory Footprint Optimization

Heap Memory

  • Off-heap memory for large data sets: ByteBuffer.allocateDirect() for network buffers, large caches, and file I/O buffers that do not need GC management
  • Primitive types over boxed types: avoid Integer, Long, and Double where int, long, and double suffice. Autoboxing creates heap allocation pressure
  • Collection pre-sizing: initialize ArrayList and HashMap with expected capacity to avoid resize operations that create intermediate arrays
  • Object pooling: appropriate for expensive-to-create objects like database connections. For short-lived value objects, modern generational GC handles allocation efficiently without pooling

Metaspace and Native Memory

  • -XX:MaxMetaspaceSize: set an explicit limit to prevent unbounded growth, particularly in frameworks that generate proxy classes at runtime such as Spring and CGLIB
  • ClassLoader leaks: in application servers and OSGi containers, failed ClassLoader unloading prevents class metadata collection. Monitor with jmap class statistics
  • Native memory tracking: use -XX:NativeMemoryTracking=detail to identify unexpected growth from JIT code cache, JNI, or direct buffer allocation

JVM Performance Optimization: Problem Areas and Solutions

Category Standard Approach Azul Prime Alternative
GC Pauses Tune G1GC flags, switch to ZGC or Shenandoah, rightsize heap C4 GC: near-zero STW pauses by design, self-tuning heap management
JIT Warmup AppCDS, synthetic warmup traffic, CRaC snapshots ReadyNow!: persistent JIT profiles across restarts
Thread Contention Profile with JFR or async-profiler, refactor locks, use concurrent collections Same approach; GC thread contention eliminated by C4 concurrent model
GC Tuning Complexity Extensive parameter tuning, environment-specific configs Self-tuning: C4 adapts automatically, minimal manual configuration
Large Heap Scalability G1GC pauses grow with heap size. ZGC helps but adds CPU overhead C4 scales to terabytes with consistent latency profile
JIT Throughput C2 compiler, code cache tuning, inlining adjustments Falcon LLVM-based JIT: more aggressive optimization than HotSpot C2
Monitoring JFR plus JMC plus external profilers and APM tools Azul Mission Control download; Azul Intelligence Cloud for fleet visibility

When Standard Tuning Is Sufficient and When It Is Not

Standard JVM tuning is sufficient for the majority of Java workloads. If latency SLAs are measured in hundreds of milliseconds or seconds, G1GC with reasonable heap sizing will meet them. Standard tuning reaches its limits in specific scenarios:

  • Latency SLAs below 10 milliseconds at P99 or P99.9: ZGC and Shenandoah improve things significantly, but tail latency from root scanning remains
  • Heaps larger than 50-100 GB: GC pause times scale with heap size even with ZGC on very large heaps
  • Significant engineering time spent on GC tuning: if teams maintain dozens of GC flags across environments, the operational cost of tuning is itself a productivity problem
  • Frequent restarts where warmup affects users: if deployments cause performance degradation periods, ReadyNow! addresses this directly

Azul Prime is a commercial product appropriate for specific problem categories. For standard workloads, Azul Zulu (free OpenJDK distribution) with G1GC or ZGC is the right answer.

A Practical Optimization Sequence

  • Establish baselines: measure P50, P95, P99, and P99.9 latency and throughput under representative load
  • Enable JFR: start continuous JFR recording in production to have diagnostic data when incidents occur
  • Identify the dominant problem category: GC pauses, warmup latency, thread contention, or memory footprint
  • Apply targeted fixes: address the dominant problem first with the lightest-weight solution available
  • Measure again: confirm improvement and check for regressions in other performance dimensions
  • Evaluate JVM-level solutions: if tuning cannot meet requirements, assess whether Azul Prime addresses the root cause more effectively

Conclusion

JVM performance optimization rewards structured diagnosis over ad-hoc parameter changes. Most problems are addressable with standard JDK tooling and GC tuning. For workloads where GC latency, warmup behavior, and heap scalability are fundamental constraints rather than tunable parameters, Azul Prime’s C4 GC, Falcon JIT, and ReadyNow! technology address these constraints at the JVM level rather than requiring continuous manual intervention.

Know your bottleneck first, then choose the right tool for the problem.