Contents
JVM Memory Areas Heap Sizing — -Xms, -Xmx & Young Generation Choosing a Garbage Collector G1GC Tuning Flags ZGC & Shenandoah Flags Metaspace Tuning Unified GC Logging Container-Aware Settings JIT & Startup Flags JVM Flags Quick Reference
The HotSpot JVM divides memory into several distinct regions. Understanding each one is essential before you change any flag, because a flag that helps one area can starve another.
| Region | What lives there | Controlled by |
|---|---|---|
| Heap | Object instances and arrays (Young + Old gen) | |
| Young Gen | Eden + two Survivor spaces; most objects die here | |
| Old Gen | Long-lived objects that survived Young GC | remainder of heap |
| Metaspace | Class metadata, method bytecode, interned strings (Java 8+) | |
| Code Cache | JIT-compiled native code | |
| Thread Stacks | One stack per thread (default 512 KB–1 MB) | |
| Direct / Off-heap | NIO ByteBuffers, Unsafe allocations, JVM internals | OS limit / |
Setting the heap size correctly is the single highest-impact JVM tuning action. Too small causes frequent GC; too large causes long pause times and wastes memory. Key heap flags:
-Xms<size> — initial (minimum) heap size. Set equal to-Xmx to avoid resize overhead in production.-Xmx<size> — maximum heap size. The most important single JVM flag.-Xmn<size> — fixes the Young Generation size (use instead of-XX:NewRatio when you need precision).-XX:NewRatio=N — Old:Young ratio. Default 2 means Old = 2×Young. Lower values give more Eden space (reduces promotion frequency).-XX:SurvivorRatio=N — Eden:Survivor ratio. Default 8 means each Survivor = Eden/8. Raise if many objects survive but die before tenuring.
Java 21 ships four production-grade collectors. The right choice depends on whether your workload is more sensitive to throughput (batch, data processing) or latency (APIs, trading, real-time).
| Collector | Flag | Pause model | Best for |
|---|---|---|---|
| Serial | Stop-the-world all phases | Single-core containers, tiny heaps (<256 MB) | |
| Parallel | Stop-the-world, multi-threaded | Batch jobs — maximise throughput, tolerate pauses | |
| G1GC | Mostly concurrent, bounded pauses | General-purpose API servers, heaps 4–32 GB | |
| ZGC | Sub-millisecond, fully concurrent | Low-latency APIs, very large heaps (>32 GB) | |
| Shenandoah | Sub-millisecond, concurrent compaction | Low-latency with Red Hat JDKs; similar to ZGC |
G1GC divides the heap into fixed-size regions (1–32 MB each) and picks the regions with the most garbage to collect first — hence "Garbage-First". It targets a pause goal rather than a fixed Young Generation size. The most important flags:
-XX:MaxGCPauseMillis=N — pause goal in milliseconds (default 200). G1 tries to meet this but it is a hint, not a hard limit. Aggressive values (<50 ms) may reduce throughput.-XX:G1HeapRegionSize=N — region size in MB (1–32, power of two). Auto-computed from heap size; set manually only when you have many large objects.-XX:G1NewSizePercent /-XX:G1MaxNewSizePercent — floor and ceiling for Young Gen as % of heap (defaults 5% / 60%).-XX:InitiatingHeapOccupancyPercent=N (IHOP, default 45) — heap occupancy % that triggers a concurrent marking cycle. Lower it if you see Full GCs.-XX:G1MixedGCCountTarget=N — how many mixed GC pauses to spread old-gen collection over (default 8).-XX:ParallelGCThreads /-XX:ConcGCThreads — STW and concurrent GC thread counts (auto-calculated from CPU count).
ZGC (Java 15+ production, Java 21 generational) performs all expensive work concurrently with application threads using colored pointer tricks. Its pauses are typically under 1 ms regardless of heap size, making it ideal for latency-sensitive workloads. Shenandoah uses a similar approach with Brooks forwarding pointers and is available on Red Hat-based JDKs.
| Flag | Default | Purpose |
|---|---|---|
| off | Enable ZGC | |
| off (Java 21 default on Java 23+) | Enable generational ZGC for much better throughput | |
| == -Xmx | ZGC tries to keep heap below this — leaves headroom for spikes | |
| 0 (disabled) | Force a GC cycle every N seconds even when idle | |
| 300 s | Seconds before ZGC returns unused heap pages to OS | |
| off | Enable Shenandoah (alternative to ZGC) | |
| satb | Incremental-update mode — lower allocation spike sensitivity |
Metaspace holds class metadata: method bytecode, constant pools, and field/method descriptors. Unlike PermGen (pre-Java 8), Metaspace grows dynamically from native memory. Without a cap it can grow unboundedly in applications that use heavy reflection, proxies (Spring, Hibernate), or dynamic class generation.
-XX:MetaspaceSize=N — initial commit size and the first GC trigger threshold (default ~20–25 MB). Not a minimum — just the first resize point.-XX:MaxMetaspaceSize=N — hard cap. Without this, a class-loader leak will consume all native memory. Set it to 256–512 MB for most apps.-XX:CompressedClassSpaceSize=N — the region for compressed class pointers (default 1 GB, requires-XX:+UseCompressedClassPointers ).
Java 9+ replaced the old
Key log tags to watch:
gc — basic pause summary (type, pause time, heap before/after)gc,heap — heap occupancy after each collectiongc,phases — G1 phase breakdown (marking, evacuation, cleanup)gc,humongous — G1 humongous object allocations (>½ region size)safepoint — all stop-the-world pauses including non-GC ones (deoptimisation, thread dumps)
Running Java in Docker or Kubernetes without container awareness causes the JVM to read host CPU and memory, leading to over-sized thread pools and under-sized heap relative to the container limit. Since Java 10 the JVM is container-aware by default, but a few flags ensure correct behaviour.
| Flag | Purpose |
|---|---|
| Read cgroup limits instead of host specs (default on Java 10+) | |
| Set Xmx as % of container memory limit | |
| Set Xms as % of container memory limit | |
| Override CPU count (useful when CPU quota is fractional) |
The JIT compiler (C1 + C2) optimises hot code at runtime. A handful of flags control compilation aggressiveness, tiering, and startup warm-up behaviour.
-XX:+TieredCompilation — enabled by default; C1 compiles quickly, C2 optimises hot methods deeply.-XX:CompileThreshold=N — invocations before a method is JIT-compiled (default 10,000 with tiered off). With tiered compilation, thresholds per tier take over.-XX:ReservedCodeCacheSize=N — max code cache size (default 240–256 MB). If it fills up the JVM de-optimises compiled code. Raise for large codebases.-Xshare:dump /-Xshare:on — Class Data Sharing (CDS). Dump the JVM class archive once, then load it mapped read-only on subsequent startups to save 200–500 ms.
All the flags from this article in one place, grouped by concern:
| Concern | Flag | Recommended value (8 GB, 4-core API) |
|---|---|---|
| Heap | ||
| G1GC | always explicit | |
| Metaspace | ||
| GC log | always on in production | |
| Container | always | |
| Code Cache |