JVM parameters (also called JVM flags, JVM options, or JVM tuning parameters) fall into three categories: standard options (-X flags), non-standard options (-XX flags), and system properties (-D flags). The -XX flags are the most numerous and impactful for performance tuning. This guide focuses primarily on -XX flags and -X heap flags that directly affect GC behavior, JIT compilation, and runtime diagnostics.
The guide is organized by functional area: memory parameters, GC selection, G1GC tuning, ZGC tuning, JIT and code cache, monitoring, and common anti-patterns. Each section includes a reference table with default values and recommended production settings.
How to Approach JVM Parameter Tuning
The single most important principle of JVM parameter tuning: measure before and after every change. Applying a parameter set copied from a blog post without baseline measurement is one of the most common causes of JVM performance regressions. Parameters that improve performance in one workload often degrade another.
The correct tuning workflow:
- Baseline: Capture GC logs, JFR recording, and application latency percentiles under representative load
- Identify bottleneck: Determine the actual limiting factor — GC pause frequency, GC pause duration, JIT warmup, thread contention, or heap exhaustion
- Target one variable: Change one parameter (or one closely related group) at a time
- Run comparison test: Apply the same load as the baseline test with the new parameter
- Verify improvement: Check that the target metric improved without degrading other metrics
- Document and commit: Record the parameter, the rationale, the before/after metrics, and the date
Parameters that are “safe to always set” include: -Xms/-Xmx (equal values), -XX:+HeapDumpOnOutOfMemoryError, and -Xlog:gc* with a rolling file. Everything else should be set with measured justification. The JVM’s defaults are carefully chosen — resist the urge to override them without evidence.
Core JVM Memory Parameters
Memory parameters control heap sizing, metaspace, and generation ratios. These are the first parameters to configure for any production deployment.
| Flag | Default | Purpose | Recommended |
|---|---|---|---|
| -Xms | Platform-dependent (~256MB or 1/64 of physical RAM) | Initial heap size | Set equal to -Xmx (e.g., -Xms8g) |
| -Xmx | Platform-dependent (~25% RAM) | Maximum heap size | Size to live set * 2-2.5x (e.g., -Xmx8g) |
| -XX:MetaspaceSize | 21MB | Initial metaspace — controls first metaspace GC trigger | 256m for most apps; 512m for large Spring apps |
| -XX:MaxMetaspaceSize | Unlimited | Cap metaspace growth | 512m–1g; prevents classloader leak OOM |
| -XX:NewRatio | 2 (1/3 young, 2/3 old) | Ratio of old to young generation size | Usually leave at default; G1GC overrides this |
| -XX:SurvivorRatio | 8 | Eden/Survivor size ratio within young gen | Increase to 16 if short-lived objects survive too long |
| -XX:+HeapDumpOnOutOfMemoryError | disabled | Write heap dump on OOM for analysis | Enable in production |
Note: -XX:NewRatio and -XX:SurvivorRatio are largely overridden by G1GC’s region-based management. They remain relevant for Parallel GC and Serial GC. When running G1GC, use G1NewSizePercent and G1MaxNewSizePercent instead.
GC Selection Parameters
The GC selection flag is the single most impactful parameter in your JVM configuration. Choose one GC algorithm and do not mix flags from different collectors.
| Flag | GC Algorithm | Java Version | Best For |
|---|---|---|---|
| -XX:+UseG1GC | Garbage-First (G1GC) | Default since Java 9 | General-purpose; balanced throughput/latency; heaps 4–32GB |
| -XX:+UseZGC | Z Garbage Collector | Experimental Java 11; Production Java 15 | Low latency (<1ms pauses); heaps up to 16TB |
| -XX:+UseShenandoahGC | Shenandoah | Available in Red Hat OpenJDK builds | Low latency; alternative to ZGC on Red Hat platforms |
| -XX:+UseSerialGC | Serial (single-threaded) | All versions | Single-core environments; tiny heaps; CLI tools |
| -XX:+UseParallelGC | Parallel (throughput) | All versions | Batch processing; throughput-maximizing; less latency-sensitive |
Java 21 introduces Generational ZGC as a sub-mode of ZGC. Enable with: -XX:+UseZGC -XX:+ZGenerational. This is the recommended ZGC configuration for Java 21+. The original non-generational ZGC mode is preserved but may be deprecated in a future release.
G1GC Tuning Parameters
G1GC is the default and most widely deployed GC algorithm. These parameters tune its pause time targets, region sizing, and concurrent marking behavior. Most applications should start with only -XX:MaxGCPauseMillis and -XX:G1HeapRegionSize and add others only with measured justification.
| Flag | Purpose | Recommended Value |
|---|---|---|
| -XX:MaxGCPauseMillis | Soft pause time goal; G1 adjusts collection set to target this | 200ms default; tune down to 100ms or 50ms after baselining |
| -XX:G1HeapRegionSize | Heap region size (1–32MB, must be power of 2) | Set so region count is 2,000–4,000; e.g., 16m for 32GB heap |
| -XX:G1NewSizePercent | Minimum percentage of heap for young generation | 5 (default); increase to 20 for high allocation rate apps |
| -XX:G1MaxNewSizePercent | Maximum percentage of heap for young generation | 60 (default); lower to 40 for large old gen applications |
| -XX:ConcGCThreads | Concurrent marking threads | Set to ~1/4 of ParallelGCThreads; e.g., 4 on 16-core host |
| -XX:ParallelGCThreads | Stop-the-world GC threads | Default is min of 8, or CPU_count/2+1; increase for high-core hosts |
| -XX:+G1UseAdaptiveIHOP | Adaptive initiating heap occupancy for concurrent marking | enabled by default; leave on |
| -XX:+UseStringDeduplication | Deduplicate identical String objects to reduce heap | Enable for apps with many duplicate strings (e.g., JSON keys) |
A critical G1GC parameter not in the table: -XX:G1MixedGCLiveThresholdPercent (default 85). Old gen regions with live data percentage below this threshold are candidates for mixed GC collection. Lowering this forces G1 to collect more old gen garbage per cycle but increases mixed GC pause duration.
ZGC Tuning Parameters
ZGC is designed to be self-tuning — it requires far fewer manual parameters than G1GC. The most important configuration for ZGC is adequate heap headroom and sufficient concurrent GC threads. Avoid over-tuning ZGC; the defaults handle most workloads.
| Flag | Purpose | Note |
|---|---|---|
| -XX:+UseZGC | Enable ZGC garbage collector | Production-ready Java 15+; use with -XX:+ZGenerational on Java 21, made generational in JDK 23+ |
| -XX:+ZGenerational | Enable Generational ZGC (Java 21+) | Strongly recommended on Java 21+; improves throughput 10% vs. classic ZGC |
| -XX:ConcGCThreads | Number of concurrent ZGC threads | ZGC auto-tunes if unset; set manually only if CPU contention is observed |
| -XX:ZAllocationSpikeTolerance | Headroom multiplier for allocation spike handling | Default 2.0; increase to 4.0 for highly bursty allocation workloads |
| -Xlog:gc* | Enable GC logging | Essential for ZGC — monitor for “Allocation Stall” events indicating GC thread saturation |
Allocation stalls are the primary ZGC failure mode under heavy load. When ZGC’s concurrent GC threads cannot reclaim memory fast enough to satisfy allocation requests, application threads stall briefly (not a traditional STW pause, but observable as latency). Remediation: increase heap headroom (-Xmx) or increase -XX:ConcGCThreads.
JIT and Code Cache Parameters
JIT compilation parameters affect both startup latency (how quickly methods reach peak compiled performance) and steady-state throughput (how aggressively the JIT optimizes hot code). The code cache is where compiled native code lives — if it fills up, JIT compilation stops and performance degrades severely.
| Flag | Purpose | Recommended |
|---|---|---|
| -XX:+TieredCompilation | Enable multi-tier JIT (C1 fast-compile + C2 optimized) | On by default since Java 8; avoid disabling |
| -XX:ReservedCodeCacheSize | Maximum size of JIT code cache | 512m for large applications; default is 240MB in Java 11+ |
| -XX:InitialCodeCacheSize | Initial code cache allocation | 64m–128m; pre-allocates cache to avoid early resizing |
| -XX:CompileThreshold | Invocations before C2 compilation (in non-tiered mode) | Leave at default (10,000) unless eliminating warmup is critical |
| -XX:+UseCodeCacheFlushing | Evict stale compiled code when cache nears capacity | Enable as a safety net; prevent full cache stop-all-compilation |
Warning signs of code cache issues: the log message “CodeCache is full. Compiler has been disabled.” or visible throughput degradation 10–30 minutes after startup (after peak compilation). Check code cache usage with: jcmd <pid> Compiler.codecache
Monitoring and Diagnostics Parameters
Monitoring parameters do not affect performance directly — they make performance problems visible. Every production JVM deployment should include at minimum GC logging. The additional flags below enable deeper diagnostic capabilities.
| Flag | Purpose |
|---|---|
| -Xlog:gc*:file=gc.log:time,uptime,level,tags:filecount=10,filesize=50m | Comprehensive GC logging to a rolling file; 10 files x 50MB each |
| -XX:+PrintCompilation | Log each JIT compilation event; useful for warmup analysis (verbose in production) |
| -XX:+UnlockDiagnosticVMOptions | Required prerequisite for some -XX:+Unlock* diagnostic flags |
| -XX:+LogCompilation | XML format detailed JIT compilation log; use with JITWatch for analysis (high overhead) |
| -XX:FlightRecorderOptions=maxsize=250m,maxage=6h | Configure JFR continuous recording retention; essential for post-incident GC analysis |
Java Flight Recorder should be enabled by default in production: -XX:StartFlightRecording=maxsize=250m,maxage=6h,dumponexit=true,filename=jfr/ The overhead is less than 2% in most workloads and the diagnostic value is enormous. JFR data is essential for correlating GC events with application-level latency spikes.
Parameters to Avoid
Certain JVM parameters are commonly seen in legacy configurations, blog posts, or cargo-culted flag sets that no longer reflect current JVM behavior. Using these can degrade performance or cause subtle bugs:
- -XX:+DisableExplicitGC — suppresses System.gc() calls. Problematic because some libraries (RMI, NIO direct buffers) rely on System.gc() for critical cleanup. Prefer -XX:+ExplicitGCInvokesConcurrent instead, which makes explicit GC concurrent rather than disabling it.
- -XX:+AggressiveOpts — was deprecated in Java 11 and removed in Java 12. Using it produces a warning and has no effect. Remove from any flag sets copied from pre-Java-11 documentation.
- -XX:+UseBiasedLocking — deprecated in Java 15 [Source: openjdk.org/jeps/374] and removed in Java 18obsoleted Java 18. Biased locking was intended to reduce lock overhead but caused JVM safepoint storms during revocation. Modern JVMs handle uncontended locking efficiently without it.
- -XX:+UseCompressedOops without -XX:+UseCompressedClassPointers — CompressedOops is on by default for heaps under 32GB. Enabling one without the other creates inconsistency. Leave both at default.
- -XX:PermSize / -XX:MaxPermSize — PermGen was replaced by Metaspace in Java 8. These flags are silently ignored in Java 8+ but indicate the configuration is from a pre-Java-8 era and should be reviewed in full.
- -server / -client — these flags are no longer meaningful. All modern JVM installations run in server mode by default.
- -XX:+PrintGCDetails without -Xlog:gc* — the old -XX:+PrintGC* flags were deprecated in Java 9 in favor of unified logging1. Use -Xlog:gc* instead.
The broader anti-pattern is setting flags without understanding what they do. Copy-pasting JVM flag sets from documentation, Stack Overflow, or old guides without validating applicability to your JVM version and workload is a significant source of production JVM problems.
When Parameters Aren’t Enough: Azul Prime
Manual JVM parameter tuning has inherent limits. Static flag configurations are optimized for a single operating point — the load conditions under which they were measured. Production workloads vary: traffic spikes, batch jobs running alongside interactive services, and shifting allocation patterns mean that a carefully tuned flag set at 50% load may perform poorly at 95% load.
Azul Prime solves this with the C4 garbage collectorZing — the Continuously Concurrent Compacting Collector. C4 is genuinely self-tuning: it continuously adapts its GC concurrency, pacing, and heap management to observed allocation and live data patterns. It requires no -XX:MaxGCPauseMillis, no G1HeapRegionSize, no ConcGCThreads tuning. Most Azul Prime deployments need only -Xms and -Xmx.
C4 is also genuinely pauseless — eliminates nearly allnear-zero stop-the-world phases. Where ZGC has brief root-scanning STW pauses and allocation stalls, C4 performs all GC phases concurrently including root scanning and compaction. This makes Azul Prime a strong choice when pause times must approach zero (not “sub-millisecond” but as close to zero as possible) and when heap sizes scale beyond what open-source JVMs can handle reliably.
Azul Prime also ships the Falcon JIT (LLVM-based, higher optimization ceiling than HotSpot C2) and ReadyNow!, which caches JIT compilation profiles across JVM restarts. ReadyNow! eliminates the need for warmup-related tuning: -XX:CompileThreshold, AppCDS, and deployment pipeline warm-up requests become unnecessary because the JVM starts at peak compiled performance after the first restart with a saved profile.
Frequently Asked Questions
What JVM flags should I set for production?
A minimal but complete production flag set: -Xms<n>g -Xmx<n>g -XX:+UseG1GC -XX:MaxGCPauseMillis=200 -XX:MetaspaceSize=256m -XX:MaxMetaspaceSize=512m -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/var/log/app/ -Xlog:gc*:file=/var/log/app/gc.log:time,uptime:filecount=10,filesize=50m. Add GC-specific tuning only after measuring baseline with these settings.
How do I reduce G1GC pause times with flags?
First, identify whether pauses are from minor GC, mixed GC, or full GC using GC logs. For minor GC pause reduction: lower -XX:MaxGCPauseMillis gradually (200 -> 100 -> 50ms) and observe if G1 can honor the target. Increase -XX:ConcGCThreads to speed up concurrent marking. For full GC elimination: increase heap headroom (-Xmx), check for humongous allocation (objects > 50% of G1HeapRegionSize), and ensure concurrent marking completes before the heap fills.
What is the recommended heap size setting?
Set -Xms and -Xmx to the same value to prevent heap resizing. Size based on your application’s live data set: use -Xmx = live data set size * 2.5. The live data set is the heap occupancy immediately after a full GC. For containerized deployments, reserve 512MB–1GB for non-heap JVM overhead (metaspace, code cache, thread stacks) and set -Xmx to the remainder of your container memory limit.
How do I enable ZGC parameters in Java 21?
Full ZGC parameter set for Java 21: -XX:+UseZGC -XX:+ZGenerational -Xms<n>g -Xmx<n>g -Xlog:gc*:file=gc.log:time,uptime:filecount=10,filesize=50m. Size heap with 20–25% extra headroom compared to your G1GC sizing. Monitor logs for “Allocation Stall” events. Do not set -XX:MaxGCPauseMillis with ZGC — it is a G1GC parameter and has no effect on ZGC behavior.
What JVM parameters does Azul Prime use?
Azul Prime requires only -Xms and -Xmx for most deployments. C4’s self-tuning architecture adapts all GC-related parameters automatically. Azul Prime does not expose -XX:MaxGCPauseMillis (it’s not needed — C4 is designed to provide near-zero pauses), GC region sizing, or concurrent thread count as user-configurable parameters because the GC manages them internally. This is intentional design: the absence of required tuning parameters is a feature, not a limitation.
Can I set JVM parameters at runtime?
Most JVM performance parameters are set at startup and cannot be changed at runtime. Exceptions include some JIT and diagnostic flags that can be toggled via jcmd: jcmd <pid> VM.flags -flag <name>=<value>. The manageable flags are a subset — heap size cannot be changed at runtime beyond the -Xmx cap, and GC algorithm cannot be changed after startup. For dynamic reconfiguration needs (e.g., adjusting MaxGCPauseMillis based on load), Azul Prime’s self-tuning C4 GC is the practical solution.
Conclusion
JVM performance tuning parameters are powerful tools that reward careful, measured application. The core principle remains constant: establish a baseline, identify the limiting factor, make one targeted change, verify improvement. The tables in this guide provide the reference — they tell you what each parameter does and reasonable starting values — but they cannot replace the measurement workflow.
For teams whose performance requirements have outgrown what manual parameter tuning can deliver, Azul Prime offers C4’s self-tuning GC, the Falcon JIT, and ReadyNow! warmup elimination. It replaces the ongoing operational burden of JVM parameter management with a platform engineered to deliver consistent performance without manual intervention.