zerve.gg
Back to blog

Aikar's JVM Flags Explained: What Each Flag Does and Why

On July 2, 2018, Daniel Ennis (Aikar), the lead developer of Paper, published a blog post titled Tuning the JVM G1GC Garbage Collector Flags for Minecraft. The flags he described became the de facto standard for Minecraft server JVM tuning and are the official recommendation in PaperMC’s documentation.

Most server admins copy-paste these flags without knowing what they do. That is fine for getting started, but understanding the reasoning lets you troubleshoot GC-related lag, adapt to different heap sizes, and evaluate newer alternatives like Generational ZGC.

This post breaks down every flag, one by one. If you’re trying to figure out whether GC is even your problem in the first place, start with how to read a Spark profiler report and check /spark gc before assuming flag tuning will help.

The complete flags

Here is the full command from the PaperMC docs:

java -Xms10G -Xmx10G \
  -XX:+UseG1GC \
  -XX:+ParallelRefProcEnabled \
  -XX:MaxGCPauseMillis=200 \
  -XX:+UnlockExperimentalVMOptions \
  -XX:+DisableExplicitGC \
  -XX:+AlwaysPreTouch \
  -XX:G1NewSizePercent=30 \
  -XX:G1MaxNewSizePercent=40 \
  -XX:G1HeapRegionSize=8M \
  -XX:G1ReservePercent=20 \
  -XX:G1HeapWastePercent=5 \
  -XX:G1MixedGCCountTarget=4 \
  -XX:InitiatingHeapOccupancyPercent=15 \
  -XX:G1MixedGCLiveThresholdPercent=90 \
  -XX:G1RSetUpdatingPauseTimePercent=5 \
  -XX:SurvivorRatio=32 \
  -XX:+PerfDisableSharedMem \
  -XX:MaxTenuringThreshold=1 \
  -Dusing.aikars.flags=https://mcflags.emc.gs \
  -Daikars.new.flags=true \
  -jar paper.jar --nogui

The two -D properties at the end (using.aikars.flags and aikars.new.flags) are informational markers. They do not affect JVM behavior. They exist so that server software and diagnostic tools can detect that Aikar’s flags are in use.

Flag-by-flag breakdown

GC selection

-XX:+UseG1GC

Selects the Garbage-First (G1) garbage collector. G1 divides the heap into equally sized regions and collects the regions with the most garbage first, which is where it gets its name. It aims to meet a configurable pause time target while maximizing throughput.

G1 has been the default collector since Java 9, so this flag is technically redundant on modern JVMs. Including it explicitly makes the intent clear and prevents surprises if defaults ever change.

Young generation sizing

Minecraft’s core problem for GC tuning is its allocation rate. According to Aikar’s analysis, a Minecraft server allocates at least 800 MB/s of short-lived objects on a 30-player server. Most of these are transient objects like BlockPosition instances that die within milliseconds.

-XX:G1NewSizePercent=30 (default: 5%)

Sets the minimum percentage of the heap reserved for the young generation. The JVM default of 5% is far too small for Minecraft’s allocation rate. At 5% of a 10 GB heap (500 MB), the young generation fills up multiple times per second, triggering constant minor GC pauses. Raising this to 30% (3 GB on a 10 GB heap) gives short-lived objects enough room to die before a collection is needed.

-XX:G1MaxNewSizePercent=40 (default: 60%)

Sets the maximum percentage of the heap that the young generation can grow to. Aikar lowers this from 60% to 40% to reserve more space for the old generation. This is a deliberate tradeoff: you want a large young generation, but not so large that the old generation gets starved and triggers expensive full GC pauses.

The combination of 30% minimum and 40% maximum gives G1 a narrow, predictable band to work within, rather than the default 5-60% range where behavior varies wildly depending on load.

Region and humongous objects

-XX:G1HeapRegionSize=8M (default: auto-calculated)

Sets the size of each G1 heap region. By default, the JVM calculates this automatically, targeting roughly 2,048 regions. For a 10 GB heap, that works out to approximately 5 MB per region.

This matters because of G1’s humongous object handling. Any allocation larger than half the region size is treated as a “humongous” allocation and gets placed directly into the old generation, bypassing the young generation entirely. With the default ~5 MB regions, any object over ~2.5 MB is humongous. Setting the region size to 8 MB raises the humongous threshold to 4 MB, which prevents many of Minecraft’s larger allocations (chunk data, NBT structures) from being prematurely promoted to old gen.

Old generation collection tuning

These flags control when and how aggressively G1 collects old generation regions.

-XX:InitiatingHeapOccupancyPercent=15 (default: 45%)

Controls when G1 starts a concurrent marking cycle to identify garbage in the old generation. The default waits until 45% of the heap is occupied before beginning. Aikar sets this to 15%, which starts the process much earlier.

Starting earlier is important because Minecraft’s high allocation rate can fill the old generation quickly. If concurrent marking starts too late, G1 may not finish identifying garbage before the old generation runs out of space, triggering a “to-space exhausted” failure and a full stop-the-world GC pause. Starting at 15% gives G1 a comfortable head start.

-XX:G1MixedGCCountTarget=4 (default: 8)

After a concurrent marking cycle identifies garbage regions in the old generation, G1 performs “mixed” collections that clean both young and old regions simultaneously. This flag controls how many mixed GC cycles G1 spreads that work across. Lowering from 8 to 4 means each mixed collection does more work but finishes the cleanup faster, reducing the window where old generation fragmentation can accumulate.

-XX:G1MixedGCLiveThresholdPercent=90 (default: 85%)

A region is eligible for collection during a mixed GC only if the percentage of live (non-garbage) data in the region is below this threshold. Raising from 85% to 90% makes more regions eligible for collection, even those that are mostly live data. This is more aggressive but helps reclaim fragmented regions that would otherwise be skipped.

-XX:G1ReservePercent=20 (default: 10%)

The percentage of heap space kept as a reserve buffer. G1 uses this reserve to handle allocation bursts that happen faster than the collector can free space. Minecraft’s spiky allocation pattern (tick processing, chunk loading) benefits from a larger reserve. Doubling it from 10% to 20% reduces the risk of “to-space exhausted” failures during load spikes.

-XX:G1HeapWastePercent=5 (default: 5%)

The percentage of heap space G1 is willing to leave uncollected. Aikar’s value matches the JVM default (changed from 10% in JDK 8u40). When reclaimable space drops below this percentage, G1 stops doing mixed collections. Keeping this at 5% ensures G1 does not leave too much garbage sitting around.

-XX:G1RSetUpdatingPauseTimePercent=5 (default: 10%)

Controls how much of the GC pause budget is spent updating remembered sets (data structures that track cross-region references). Lowering from 10% to 5% shifts more of this work to concurrent background threads, reducing the stop-the-world portion of each pause.

Object promotion

-XX:MaxTenuringThreshold=1 (default: 15)

Controls how many young generation collections an object must survive before being promoted to the old generation. The JVM default of 15 means an object could stay in the young generation through up to 15 collection cycles.

Aikar sets this to 1, which means: if an object survives one young generation collection, promote it immediately. This sounds aggressive, but it matches Minecraft’s allocation pattern perfectly. The vast majority of Minecraft’s objects are either truly transient (dead within one GC cycle) or long-lived (server state, loaded chunks, cached data). Very few objects fall in between. Setting the threshold to 1 avoids wasting survivor space copying objects back and forth between survivor regions when they are going to end up in old gen anyway.

-XX:SurvivorRatio=32 (default: 8)

Controls the ratio between eden space and each survivor space within the young generation. A ratio of 8 means each survivor space gets 1/10th of the young generation (eden gets 8/10, two survivor spaces get 1/10 each). A ratio of 32 means each survivor space gets 1/34th of the young generation.

Since MaxTenuringThreshold=1 causes objects to promote after a single survival, the survivor spaces do not need to hold objects for multiple cycles. Making them smaller gives more room to eden, where all new allocations happen. Combined with the high allocation rate, a larger eden means fewer young generation collections per second.

Pause target

-XX:MaxGCPauseMillis=200 (default: 200ms)

The target maximum GC pause time. This is the same as the JVM default. G1 uses this target to decide how many regions to collect in each pause. It is a target, not a guarantee. The real value of Aikar’s flags is not in changing this target but in tuning all the other parameters so that G1 can actually meet it consistently.

Miscellaneous

-XX:+UnlockExperimentalVMOptions

Required to use experimental flags like G1NewSizePercent and G1MaxNewSizePercent. Without this, the JVM rejects those flags at startup.

-XX:+DisableExplicitGC

Ignores calls to System.gc() in application code. Some plugins or libraries call System.gc() explicitly, which forces a full collection regardless of whether one is needed. This flag prevents that from causing unnecessary pauses.

-XX:+AlwaysPreTouch

Forces the JVM to touch (allocate and zero) all heap pages at startup rather than lazily on first use. This means the full heap is mapped into physical memory immediately. The benefit is that memory access during runtime is faster and more predictable since the OS has already committed the pages.

The tradeoff is longer startup time (a few seconds on large heaps) and the fact that the JVM’s resident memory footprint equals the full heap size from the moment it starts. In containerized environments (Docker, Pterodactyl), if the container’s memory limit is set too close to the heap size, AlwaysPreTouch can cause the container to be OOM-killed at startup because the JVM tries to commit the entire heap plus its own overhead before the server even loads. If you run in a container, ensure the memory limit is at least 1-1.5 GB above your -Xmx value.

-XX:+PerfDisableSharedMem

Disables the JVM’s shared memory performance counters, which are stored as a memory-mapped file in /tmp/hsperfdata_<username>. The problem: on Linux, writes to memory-mapped files can block until disk I/O completes, even if the I/O is to a completely different disk. Since the JVM updates these counters during GC pauses, a slow /tmp filesystem can add hundreds of milliseconds to what should be a sub-200ms pause. Disabling this eliminates that risk. The cost is that tools like jstat and jps will no longer be able to find the process, but those are debugging tools you rarely need on a production Minecraft server.

-XX:+ParallelRefProcEnabled

Enables parallel (multi-threaded) processing of Java references (WeakReference, SoftReference, PhantomReference) during GC pauses. This flag has been enabled by default since JDK 11 for G1 and since JDK 17 for the Parallel collector. Including it explicitly ensures the behavior on older JVMs.

Note: this flag is deprecated in JDK 26 and scheduled for removal. Parallel reference processing is now always on. If you run JDK 26+, the flag will produce a deprecation warning but still work. It will be obsoleted in JDK 27 and removed in JDK 28.

The >12 GB variant

For servers allocated more than 12 GB of heap, Aikar recommends adjusting five flags:

FlagStandard>12 GB
G1NewSizePercent3040
G1MaxNewSizePercent4050
G1HeapRegionSize8M16M
G1ReservePercent2015
InitiatingHeapOccupancyPercent1520

The reasoning: with more total heap, you can afford to give the young generation a larger share (40-50%) without starving the old generation. Larger regions (16M) raise the humongous threshold to 8 MB. The reserve can be smaller in percentage terms because 15% of 16 GB (2.4 GB) is still more absolute space than 20% of 10 GB (2 GB). And IHOP can be slightly higher because there is more headroom for concurrent marking to complete before the old generation fills up.

Heap sizing

Xms must equal Xmx. Every time the JVM resizes the heap from -Xms toward -Xmx, it triggers a GC pause and may need to defragment memory. Setting them equal pre-allocates the full heap at startup and eliminates resize pauses entirely. As Aikar puts it: unused memory is wasted memory, and G1 operates better with more memory available.

Recommended range: 6-12 GB. Below 6 GB, the young generation percentages in Aikar’s flags (30-40% of heap) do not leave enough absolute space for the old generation to work comfortably. Above 12 GB, use the large-heap variant described above.

Reserve host memory for JVM overhead. The -Xmx value controls only the Java heap. The JVM also needs memory for thread stacks, metaspace, code cache, direct buffers, and native allocations. On a dedicated host or container, reserve at least 1-1.5 GB above your -Xmx value. If your host has 12 GB of RAM, set -Xmx to 10G, not 12G.

Common mistakes

Xms != Xmx. The single most common misconfiguration. Our analysis of 7,661 real server profiles found that 15.4% of servers have -Xms set below -Xmx. This causes heap resize pauses under load, exactly when you can least afford them.

Allocating 100% of host RAM. Setting -Xmx equal to total system memory leaves nothing for the OS, the JVM’s own overhead, or other processes. The server may run fine for hours and then get OOM-killed during a load spike when native memory demand exceeds what is left.

AlwaysPreTouch in containers with tight limits. In Docker or Pterodactyl, the container memory limit must account for both the heap and JVM overhead. If you set a 10 GB heap and a 10 GB container limit, AlwaysPreTouch will commit the full heap at startup, leaving zero room for the JVM itself. The container gets killed before the server finishes loading.

Mixing G1 flags with ZGC. If you switch to -XX:+UseZGC, every G1-specific flag (G1NewSizePercent, G1HeapRegionSize, InitiatingHeapOccupancyPercent, etc.) is silently ignored. The JVM does not warn you. You end up running ZGC with its defaults while thinking you have a tuned configuration. Always start from a clean flag set when switching collectors.

Using Aikar’s flags on a heap below 4 GB. The young generation sizing (30-40% of heap) assumes a heap large enough for the absolute sizes to work. At 4 GB, the young generation gets 1.2-1.6 GB and the old generation gets 2.4-2.8 GB minus the 20% reserve. That is workable but tight. Below 4 GB, the margins become too thin and you may see frequent full GC pauses.

When to move beyond Aikar’s flags

Aikar’s flags were designed for G1GC and published when Java 8 was the standard. They remain excellent defaults, but the GC landscape has changed:

For a deeper comparison of G1 versus ZGC for Minecraft, including benchmark data and a decision matrix by heap size, see Best JVM Flags for Minecraft Servers in 2026.

Sources