JVM Flags Explained: Standard, -X, and -XX Options for Tuning
Master JVM flags configuration with this comprehensive guide covering standard, -X, and -XX options for production Java performance tuning.
Introduction
The Java Virtual Machine offers dozens of flags to control runtime behavior, garbage collection, memory allocation, and JIT compilation. Understanding which flags exist and when to use them separates ad-hoc tuning from deliberate, measurable optimization. This guide covers the three flag categories, practical tuning patterns, and the failure modes you’ll encounter in production.
When to Use This / When Not to Use This
Use JVM flags when:
- You need to tune heap size based on actual memory pressure observations
- GC pause times are causing SLA violations and you need to select or configure a specific collector
- You are diagnosing a specific issue (e.g., class metadata space exhaustion, stack overflow)
- You want to enable diagnostic output for post-incident analysis
Do not use JVM flags when:
- You have not measured the problem first. Tuning without data is guesswork.
- Your application is undersized at the architecture level. More heap does not fix a flawed design.
- You are applying “magic numbers” copied from blog posts without understanding the tradeoffs.
JVM Flag Categories
JVM flags fall into three categories based on stability and intended audience:
| Category | Prefix | Stability | Audience |
|---|---|---|---|
| Standard | None | Guaranteed across all implementations | All users |
| Non-Standard | -X | May change between releases | Advanced users |
| Developer/Experimental | -XX | Often undocumented, changeable | JVM engineers, specialists |
Standard options follow standard naming conventions and are guaranteed to be available. Non-standard options use the -X prefix. Developer and experimental options use -XX, which is also where most performance tuning flags live.
Standard Options
Standard options begin with a single dash and behave like traditional Java flags:
java -cp myapp.jar com.example.MyApp
java -version
java -help
Common standard options for tuning include:
-ea/-enableassertions- Enable assertions-da/-disableassertions- Disable assertions-server- Server JVM selector (default for server-class machines)-client- Client JVM selector (removed in Java 12)
Non-Standard Options (-X)
Non-standard options use the -X prefix and are specific to the HotSpot JVM:
java -Xms512m -Xmx4g -Xss256k -Xprof myapp.jar
Key -X flags include:
-Xms<size>- Initial heap size-Xmx<size>- Maximum heap size-Xss<size>- Thread stack size-Xmn<size>- Young generation size (for parallel GC)-Xlog:gc<options>- GC logging (replaces-XX:+PrintGCDetailsin modern JVMs)-Xshare- Class data sharing-Xrun<hprof:options>- HPROF profiler integration
Developer/Experimental Options (-XX)
The -XX namespace contains the majority of tuning flags. These are organized by subsystem:
Heap and Memory Flags
-XX:InitialHeapSize=<size> # Equivalent to -Xms
-XX:MaxHeapSize=<size> # Equivalent to -Xmx
-XX:NewSize=<size> # Initial young generation size
-XX:MaxNewSize=<size> # Maximum young generation size
-XX:SurvivorRatio=<ratio> # Eden/Survivor space ratio
-XX:MetaspaceSize=<size> # Initial metadata space
-XX:MaxMetaspaceSize=<size> # Maximum metadata space
-XX:MinHeapFreeRatio=<percent> # Minimum heap free percentage
-XX:MaxHeapFreeRatio=<percent> # Maximum heap free percentage
Garbage Collection Flags
# Collector selection
-XX:+UseSerialGC # Serial collector (single thread)
-XX:+UseParallelGC # Parallel collector (throughput)
-XX:+UseParallelOldGC # Parallel old generation collector
-XX:+UseConcMarkSweepGC # CMS collector (deprecated in Java 9)
-XX:+UseG1GC # G1 garbage collector
-XX:+UseZGC # ZGC (low-latency)
-XX:+UseShenandoahGC # Shenandoah (low-latency, no forwarders)
# G1 specific
-XX:MaxGCPauseMillis=<n> # Target maximum GC pause
-XX:G1HeapRegionSize=<n> # G1 region size (1MB-32MB)
-XX:ParallelGCThreads=<n> # Threads for parallel phases
# ZGC specific
-XX:+ZGenerational # Enable generational ZGC (Java 21+)
JIT Compiler Flags
-XX:+TieredCompilation # Enable tiered compilation
-XX:TieredStopAtLevel=<n> # Stop at specific compilation tier
-XX:CompileThreshold=<n> # Invocation threshold before compilation
-XX:OnStackReplacePercentage=<n> # OSR trigger threshold
-XX:+PrintCompilation # Print compilation log
Architecture: JVM Flag Processing Flow
Understanding how flags flow through the JVM helps diagnose configuration errors:
graph TD
A[JVM Process Start] --> B[Argument Parsing]
B --> C{Flag Type?}
C -->|Standard| D[Apply Standard Options]
C -->|Non-Standard -X| E[Apply -X Options]
C -->|Developer -XX| F[Apply -XX Options]
D --> G[Initialize subsystems]
E --> G
F --> G
G --> H[Verify constraints]
H --> I{Valid?}
I -->|No| J[Print error and exit]
I -->|Yes| K[JVM Ready]
J --> L[Show relevant diagnostic]
Production Failure Scenarios
Scenario 1: Heap Size Mismatch with Container Limits
Problem: Setting -Xmx8g inside a container with a 4GB memory limit causes the OOM killer to terminate the process.
# Container has 4GB limit, but JVM thinks it can use 8GB
docker run -m 4g java -Xmx8g -jar myapp.jar
Solution: Always set -Xmx and -Xms to values within container limits. Account for native memory usage outside the Java heap:
# Reserve ~10-15% for native overhead
docker run -m 4g java -Xmx3500m -Xms3500m -XX:MaxRAM=4g -jar myapp.jar
Scenario 2: G1 Region Size Misconfiguration
Problem: -XX:G1HeapRegionSize=64m fails because region sizes must be powers of 2 between 1MB and 32MB.
# Invalid: 64m is not a valid G1 region size
java -XX:G1HeapRegionSize=64m -jar myapp.jar
# Error: Invalid region size: 64M. Must be between 1M and 32M
Solution: Use valid region sizes:
java -XX:G1HeapRegionSize=16m -jar myapp.jar
Scenario 3: CMS Collector Removed but Flag Retained
Problem: Running Java 14+ with -XX:+UseConcMarkSweepGC causes an error since CMS was removed.
# Works in Java 8/11, fails in Java 14+
java -XX:+UseConcMarkSweepGC -jar myapp.jar
# Error: VM option 'UseConcMarkSweepGC' is deprecated and will be removed in a future release.
# Error in Java 14+: Removed option.
Solution: Migrate to G1 or ZGC:
# Modern replacement for CMS
java -XX:+UseG1GC -XX:MaxGCPauseMillis=200 -jar myapp.jar
Scenario 4: Tiered Compilation Memory Overhead
Problem: Tiered compilation causes higher memory usage, leading to OOM in memory-constrained environments.
# Tiered compilation can add 100-300MB overhead
java -XX:+TieredCompilation -XX:ReservedCodeCacheSize=256m -Xmx512m -jar myapp.jar
Solution: Disable tiered compilation or tune code cache:
# For memory-constrained environments
java -XX:-TieredCompilation -Xmx512m -XX:ReservedCodeCacheSize=128m -jar myapp.jar
Trade-off Table
| Flag Pattern | Benefit | Cost | When To Use |
|---|---|---|---|
-Xms == -Xmx | Eliminates resize overhead, predictable memory | No memory breathing room | Containers, stable loads |
-XX:+UseSerialGC | Low memory overhead | Single-threaded, long pauses | Heap < 100MB, single-core |
-XX:+UseParallelGC | High throughput | Long GC pauses | Batch processing, no latency requirement |
-XX:+UseG1GC | Balanced, predictable pauses | More CPU overhead than Parallel | General purpose, latency-sensitive |
-XX:+UseZGC | Sub-millisecond pauses | Higher memory overhead (~10-20%) | Ultra-low latency requirements |
-XX:+UseShenandoahGC | Concurrent GC without forwarders | Lower throughput than ZGC | Medium-latency, openjdk users |
-XX:+TieredCompilation | Faster warmup, better JIT | Higher memory, longer startup | Long-running services |
-XX:-TieredCompilation | Faster startup, lower memory | Slower peak performance | Short-lived processes, serverless |
Implementation Snippets
Minimal Production Configuration
java \
-Xms2g -Xmx2g \
-XX:NewRatio=2 \
-XX:+UseG1GC \
-XX:MaxGCPauseMillis=200 \
-XX:G1HeapRegionSize=8m \
-XX:ParallelGCThreads=8 \
-XX:MetaspaceSize=256m \
-XX:MaxMetaspaceSize=512m \
-Xlog:gc*:file=/var/log/gc.log:time,uptime,level,tags \
-XX:+HeapDumpOnOutOfMemoryError \
-XX:HeapDumpPath=/var/log/heap.hprof \
-jar myapp.jar
Container-Optimized Configuration
java \
-XX:+UseContainerSupport \
-XX:MaxRAMPercentage=75.0 \
-XX:InitialRAMPercentage=75.0 \
-XX:+UseG1GC \
-XX:MaxGCPauseMillis=300 \
-Xlog:gc:/var/log/gc.log \
-XX:+ExitOnOutOfMemoryError \
-jar myapp.jar
The -XX:+UseContainerSupport flag (Java 10+) enables the JVM to detect container memory limits. Combined with MaxRAMPercentage, this avoids hard-coding values that may not match deployment environments.
GC Log Rotation Configuration
java \
-Xlog:gc*:file=/var/log/gc.log:time,uptime,level,tags:filecount=10,filesize=100m \
-jar myapp.jar
This rotates GC logs every 100MB, keeping 10 files before removal.
Observability Checklist
Before and after changing JVM flags, verify these signals:
- Monitor GC pause times with
jstat -gcutil <pid> 1000 - Check heap usage patterns with
jcmd <pid> VM.native_memory summary - Verify flag application with
java -XX:+PrintFlagsFinal -version 2>&1 | grep <flag> - Review GC logs for actual pause times vs. target
- Measure application throughput under realistic load
- Confirm container memory limits align with JVM heap settings
- Verify code cache is not becoming a bottleneck
- Check metaspace usage for class metadata leaks
Security and Compliance Notes
Some -XX flags carry security implications:
-XX:+AllowRedefinitionToAddDeleteMethods- Can be exploited for deserialization attacks-XX:+EnableMethodHandles/-XX:+EnableInvokeGeneric- Internal flags that should not be exposed in untrusted contexts-XX:+PrintFlagsFinaloutput can reveal configuration details useful for attack planning
In multi-tenant environments, restrict flag access through JVM management interfaces rather than command-line exposure.
Do not enable diagnostic flags in production unless actively troubleshooting. Flags like -XX:+UnlockDiagnosticVMOptions expand the attack surface.
Common Pitfalls / Anti-Patterns
1. Treating heap size as the only tuning knob.
Heap size affects garbage collection frequency, not underlying memory allocation patterns. Applications with poor object locality or excessive allocation rates will not improve with more heap.
2. Using -XX:+UseSerialGC in multi-core production environments.
Serial GC is almost never appropriate for production services. It uses a single thread for both application and GC work, causing severe pause times under load.
3. Setting -XX:MaxGCPauseMillis without understanding its effect.
This is a target, not a guarantee. If the JVM cannot meet the pause target with the current workload and heap size, it will not fail—it will simply not achieve the target.
4. Confusing -Xms and -XX:InitialHeapSize.
These are equivalent; -Xms is the shorthand form. Mixing both in the same command line is redundant, not additive.
5. Forgetting that native memory is separate from heap.
Metaspace, code cache, thread stacks, direct byte buffers, and JVM internal structures all consume native memory outside the Java heap. A “4GB heap” JVM may actually use 5-6GB total.
6. Applying blog “magic numbers” without measurement.
Every workload is different. Flags that work for one application may cause regressions in another.
Quick Recap Checklist
- JVM flags have three stability categories: standard, -X, -XX
- Standard options have no prefix and are guaranteed across JVM implementations
- -X flags are non-standard but stable; -XX flags are developer/experimental options
- Most GC and JIT tuning happens in the -XX namespace
- Always match -Xmx to container memory limits when running in containers
- Use
-XX:+UseContainerSupportwith percentage-based heap sizing for containerized workloads - CMS collector is deprecated and removed in modern JVMs; use G1 or ZGC instead
- Tiered compilation improves peak performance but increases memory usage
- Native memory usage (metaspace, code cache, threads) is separate from heap
- Always measure before and after flag changes
Interview Questions
The -X prefix marks non-standard options that are specific to the HotSpot JVM. These are stable but may change between releases. The -XX prefix marks developer and experimental options that control specific subsystems like the JIT compiler, garbage collector, and memory management. The -XX flags are the primary tuning surface for performance optimization, but they carry no stability guarantees and some are undocumented.
Use -XX:+UseContainerSupport (enabled by default in Java 10+) combined with percentage-based sizing like -XX:MaxRAMPercentage=75.0. This allows the JVM to detect container memory limits automatically. Alternatively, set explicit -Xmx values accounting for native memory overhead—typically reserving 10-15% of the container limit for metaspace, code cache, and thread stacks. Always verify with actual memory measurements using NativeMemoryTracking or container-level monitoring.
Setting -Xms smaller than -Xmx allows the heap to grow dynamically up to the maximum. The JVM starts with the initial size and expands as needed until hitting the maximum. This "heap breathing" has a cost: memory allocation system calls and potential GC thrashing during growth phases. For long-running services with stable load, setting them equal (-Xms == -Xmx) eliminates resize overhead and provides more predictable behavior, at the cost of always consuming the maximum memory.
CMS (Concurrent Mark Sweep) was deprecated in Java 9 and removed in Java 14. It suffered from fragmentation issues and required careful tuning to avoid "concurrent mode failure." The replacement is G1 (Garbage First), which divides the heap into regions and provides better pause time predictability. For applications requiring ultra-low latency, ZGC offers sub-millisecond pauses with maximum throughput sacrificed, while Shenandoah provides similar latency characteristics with smaller memory overhead.
Native Memory Tracking (NMT) tracks memory usage outside the Java heap, including metaspace, code cache, thread stacks, JIT compiled code, and internal JVM structures. Enable it with -XX:NativeMemoryTracking=summary or =detail. Use jcmd <pid> VM.native_memory summary to view allocations. NMT adds 5-10% overhead, so disable it in performance tests. Enable it when debugging unexplained memory growth or confirming that heap settings leave adequate native memory.
Tiered compilation uses two JIT compilers: C1 compiles quickly with minimal optimization for initial hot-method invocation, and C2 applies aggressive optimizations for methods that remain hot. This gives fast startup from C1 and peak performance from C2. Disable tiered compilation only in memory-constrained environments where the 100-300MB code cache overhead is problematic, or for short-lived processes where startup speed matters more than peak performance.
G1GC requires more tuning effort—you must set -XX:MaxGCPauseMillis appropriately, choose region sizes with -XX:G1HeapRegionSize, and balance young generation sizing with -XX:NewRatio. ZGC is designed to work with minimal tuning; its pause time goals are achieved through concurrent marking and relocation without requiring application cooperation. ZGC trades higher memory overhead (~10-20% more) for consistent sub-millisecond pauses.
-XX:+UseContainerSupport enables the JVM to detect memory limits imposed by Linux cgroups in containerized environments. When enabled, the JVM queries the container for memory limits rather than relying on the operating system's view of available memory. This allows JDK 10+ to set default heap sizes that respect container limits automatically, and makes the JVM aware of container memory so it can avoid triggering the OOM killer by respecting the cgroup boundary.
Use java -XX:+PrintFlagsFinal -version 2>&1 | grep to see the final value of any flag after JVM initialization. This shows both the flag value you set and the computed result after the JVM applies defaults and constraints. For a complete dump of all flags, run java -XX:+PrintFlagsFinal -version 2>&1 and search for specific flags.
-XX:+HeapDumpOnOutOfMemoryError triggers a heap dump automatically when the JVM hits an OutOfMemoryError exception. Without it, you have no memory snapshot at the moment of failure—making incident diagnosis significantly harder. Combined with -XX:HeapDumpPath to specify the output location, this flag provides the definitive forensic data for post-incident analysis of memory leaks, allocation bugs, or capacity planning failures.
Unlike heap sizing where -Xms and -Xmx are independent, metaspace sizing works differently. -XX:MetaspaceSize sets the initial threshold that triggers metaspace garbage collection when the committed metaspace reaches that size. The JVM then reclaims classloader metadata and may or may not return memory to the OS. -XX:MaxMetaspaceSize caps the committed metaspace. If set too low, you get OutOfMemoryError: Metaspace. If unset, metaspace can grow to consume native memory.
Key JIT-related flags include: -XX:+TieredCompilation enables tiered compilation (enabled by default in Java 8+); -XX:CompileThreshold= sets invocation count before compilation; -XX:+PrintCompilation prints compiled methods; -XX:CICompilerCount= sets the number of compiler threads; -XX:+DoEscapeAnalysis enables escape analysis (on by default). These let you control JIT warmup, compilation aggressiveness, and diagnostic output.
The code cache holds JIT compiled code native binaries. -XX:ReservedCodeCacheSize sets the maximum. If the code cache fills up, the JVM disables JIT compilation—applications suddenly become much slower. Monitor with jcmd . For applications with many JIT-compiled methods, increase the code cache. The default is 48MB for the tiered compiler. -XX:InitialCodeCacheSize sets the starting size.
Serial GC uses a single thread for both application and GC work. Appropriate only for heaps under 100MB, single-core processors, or applications where GC pauses of several seconds are acceptable. In modern production, use it only for tiny microservices with strict memory constraints (under 100MB heap), legacy applications on single-core VMs, or development environments where simplicity matters more than performance.
-Xss (or -XX:ThreadStackSize) controls the thread stack size per Java thread. Default is typically 1MB. Reducing it allows more threads per process (useful for high-concurrency servers) but risks StackOverflowError if recursion depth is high. Increasing it prevents overflow errors but reduces total threads possible given fixed native memory. For most applications, the default 1MB works well.
-XX:+PrintFlagsFinal outputs the complete list of all JVM flags with their final values after JVM initialization, including those set explicitly, those computed from other flags, and default values. -XX:+PrintFlagsInitial shows only the initial values before any argument processing. Use -XX:+PrintFlagsFinal to verify exactly which flag values are in effect for a running JVM.
-XX:+AlwaysPreTouch touches all heap pages during JVM startup, forcing them into memory physically. Without it, pages are allocated on first access. Pre-touching adds startup time but eliminates page-fault latency during runtime, which is important for latency-sensitive applications where unexpected page faults could cause jitter. In containerized environments, pre-touching also helps establish accurate memory usage before applying container limits.
Large pages (also called huge pages) allow the OS to manage memory in 2MB blocks instead of the standard 4KB pages. This reduces Translation Lookaside Buffer (TLB) misses because more memory can be mapped with fewer TLB entries. Enable with -XX:+UseLargePages and configure the OS to have sufficient huge pages available. Best for applications with large heaps (8GB+) where TLB pressure is measurable. Requires root access to configure OS huge pages.
This flag controls how long the garbage collector keeps soft references to objects. The value represents milliseconds per megabyte of free heap space. A higher value means soft references are cleared more reluctantly, keeping caches alive longer. The default is 1000ms. Setting it to 0 clears soft references aggressively, while very high values can cause excessive memory retention. Tune based on whether your application relies on soft references for caching.
When facing OOM errors, enable heap dumps with -XX:+HeapDumpOnOutOfMemoryError and -XX:HeapDumpPath to capture memory state at failure. Use -XX:+PrintGCDetails and -XX:+PrintGCTimeStamps to see if GC is working properly before the OOM. For metaspace issues, enable NMT with -XX:NativeMemoryTracking=detail. The error message itself indicates the OOM type: HeapSpace, Metaspace, or direct buffer. Use these signals to narrow down whether the issue is heap sizing, classloader leaks, or native memory exhaustion.
Further Reading
- JVM Flags Official Documentation — Oracle’s comprehensive reference for all JVM flag categories and their current status across Java releases.
- Garbage Collection Tuning Guide — Oracle’s official GC tuning documentation covering all major collectors including G1, ZGC, and Shenandoah.
- Java 21 ZGC Documentation — Detailed reference for ZGC flags including the new generational ZGC mode.
- OpenJDK Source Code (flags.hpp) — The actual JVM flag definitions maintained by the OpenJDK community for the adventurous.
- async-profiler GitHub — A production-grade profiler that works without artificial markers, useful for validating JVM flag effects in live systems.
Conclusion
JVM flags provide the primary tuning surface for garbage collection, JIT compilation, heap sizing, and diagnostic output. Understanding the three flag categories (standard, -X, -XX) helps you make informed decisions about which options to use in production. The defaults work reasonably well for many workloads, but production systems typically require explicit GC and memory configuration.
The key to effective JVM tuning is measurement before and after changes. Use GC logs, async-profiler, and JFR to understand your specific workload characteristics, then apply flags based on evidence rather than inherited wisdom. When working with containers, always use -XX:+UseContainerSupport and percentage-based heap sizing to align with container limits.
Category
Related Posts
Java Flight Recorder: Continuous Monitoring and Diagnostics
Learn how Java Flight Recorder captures low-level diagnostics, profiling data, and continuous monitoring events from the JVM in production environments.
JVM Stack Walking API: Fast Stack Traversal and Security Context
A guide to the JVM Stack Walking API showing how to efficiently traverse stack frames, access local variables, and extract security context without the overhead of traditional stack trace capture.
Deoptimization Debugging: When JIT Compiled Code Reverts
Learn what causes the JVM to deoptimize JIT-compiled code, how to detect deoptimization events, and how to fix the underlying issues.