JVM flags for Minecraft, demystified

Apr 20, 20256 min read

JVM flags for Minecraft, demystified

You've seen the line. It looks like a small explosion:

-XX:+UseG1GC -XX:+ParallelRefProcEnabled -XX:MaxGCPauseMillis=200 -XX:+UnlockExperimentalVMOptions ...

This is "Aikar's flags," named after the Paper developer who popularized them, and they are pasted into approximately every Minecraft startup script on the internet. Most people who use them have no idea what they do. That's mostly fine. They are sensible defaults. But knowing what each chunk does makes you better at recognizing when to deviate.

The 30-second version

JVM flags are tuning knobs for Java's garbage collector and runtime. Minecraft is a "stop-the-world" type workload: when the JVM pauses for garbage collection, the game pauses too. Long pauses = lag. Short pauses = smooth.

Aikar's flags tune the G1 garbage collector to favor many short pauses instead of fewer long ones. For Minecraft, this trade is correct.

Fewer, longer pauses
200-800 ms each

Many short pauses
under 100 ms each

Garbage collection budget

Pause profile preferred?

JVM defaults
not for Minecraft

Aikar-tuned G1GC
right for Minecraft

Players feel each pause
as a visible stutter

Imperceptible at 100 ms or less

The flags, group by group

Picking the collector

-XX:+UseG1GC

Use the G1 (Garbage-First) collector. This is the right collector for Minecraft up through heap sizes of roughly 12 GB. Beyond that, ZGC starts winning on pause times. G1 is the default in modern Java anyway, so this is mostly explicit.

-XX:+UnlockExperimentalVMOptions

Allows the next group of flags to be set. They are not really "experimental" anymore but the JVM still classifies them that way.

Tuning G1 for Minecraft

-XX:MaxGCPauseMillis=200

Asks G1 to aim for pauses under 200 ms. It's a soft target. Reasonable for Minecraft, where a 200 ms pause is barely perceptible.

-XX:G1NewSizePercent=30
-XX:G1MaxNewSizePercent=40

Sets the young generation to 30 to 40 percent of the heap. The young generation is where new objects live until they survive a few GC cycles. Minecraft allocates many short-lived objects (block updates, packet objects), so a bigger young generation reduces how often things have to be promoted to the old generation.

How the heap is divided, with Aikar's flags on an 8 GB heap:

35%45%20%8 GB heap, Aikar-tuned layout (% of heap)Young generation (Eden + Survivor, short-lived objects) [35]Old generation (loaded chunks, plugin state, entity refs) [45]Reserve (promotion headroom, G1ReservePercent) [20]
-XX:G1HeapRegionSize=8M

Divides the heap into 8 MB regions. Larger than the default. Helps when Minecraft allocates large objects (big chunks, big collections). Default heuristics often pick 4 MB or 2 MB, which is too small for our workload.

-XX:G1ReservePercent=20

Reserves 20 percent of the heap as headroom for promotion. Prevents the "to-space exhaustion" failure mode under high allocation pressure.

-XX:G1HeapWastePercent=5

How much waste G1 will tolerate before starting a mixed GC. Lower means more aggressive cleanup. Trades a tiny bit of throughput for steadier behavior.

-XX:G1MixedGCCountTarget=4

Try to complete mixed GC in 4 cycles. Spreads work out.

-XX:InitiatingHeapOccupancyPercent=15

Starts a concurrent GC cycle when 15 percent of the heap is used. Very aggressive. This is the most "Aikar-specific" choice. It tells G1 to start cleaning earlier than the default 45 percent, trading throughput for predictability. For Minecraft, this is right.

-XX:G1MixedGCLiveThresholdPercent=90
-XX:G1RSetUpdatingPauseTimePercent=5

Fine tuning for mixed GC behavior. Safe to leave alone.

Surviving allocation spikes

-XX:SurvivorRatio=32
-XX:+PerfDisableSharedMem
-XX:MaxTenuringThreshold=1

MaxTenuringThreshold=1 means objects get promoted to old generation after surviving one young GC. Default is 15. Aikar's setting is right for Minecraft because most surviving objects are genuinely long-lived (chunks, entities) and there's no point ping-ponging them in the young gen.

PerfDisableSharedMem disables a JVM feature that wrote performance counters to a shared memory file, which on some systems caused disk I/O hiccups. Mostly historical at this point but still safe to keep.

Heap size: the one knob most people get wrong

Heap size is set with -Xms (initial) and -Xmx (max). Standard advice: set them equal. For example:

-Xms6G -Xmx6G

Why equal? Because if min and max differ, the JVM will grow and shrink the heap, and the resizing operations are expensive. Set both to the same value.

How much to give? See the RAM-per-player article. Short version:

  • Vanilla / Paper, small groups: 3 to 4 GB.
  • Medium modded: 6 to 8 GB.
  • Heavy modded: 10 to 12 GB.

Do not allocate all of your system's RAM to the heap. Leave headroom for the OS, the JVM itself (which uses memory outside the heap), and any other services on the box. An 8 GB host should not run an 8 GB heap. 6 GB is the right answer.

When to deviate from Aikar's flags

The flags assume:

  • Heap between 4 and 12 GB.
  • A "normal" Minecraft workload.
  • The G1 collector is the right collector.

Under 2 GB

2 to 12 GB

Over 12 GB

Yes

No

Picking GC strategy

Heap size?

Use JVM defaults
Aikar flags target larger heaps

Use G1 with Aikar's flags

Switch to ZGC
UseZGC + ZGenerational

Java 21 plus,
recent CPU?

Can drop IHOP and
G1NewSizePercent. Most leave them in

Keep flags as-is

Your heap is larger than 12 GB. Switch to ZGC.

-XX:+UseZGC
-XX:+ZGenerational

ZGC has sub-millisecond pause times even at 30+ GB heaps. The tradeoff is slightly higher CPU and slightly higher memory overhead. For very large heaps it is unambiguously better than G1.

Your heap is under 2 GB. Don't bother with most of these flags. Use the JVM defaults. The flags are tuning for behavior G1 only really exhibits at larger heaps.

You're running Java 21+ on a recent CPU. Several G1 defaults have improved. You can probably remove InitiatingHeapOccupancyPercent and G1NewSizePercent and get similar results. Most people leave them in for safety.

For an 8 GB Paper / Forge server on Java 21:

java -Xms8G -Xmx8G \
  -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 server.jar nogui

For a heap above 12 GB, swap to:

java -Xms16G -Xmx16G \
  -XX:+UseZGC -XX:+ZGenerational \
  -XX:+AlwaysPreTouch \
  -jar server.jar nogui

How to know if your flags are working

Monitor:

  • Average TPS during normal play. Should sit at 20 with occasional brief dips.
  • GC pause times. Modern Paper exposes this via metrics. You want average pauses under 100 ms, max pauses under 300 ms.
  • Heap usage post-GC. Should sit at 30 to 60 percent of max. Higher means undersized. Lower means oversized.

If TPS is healthy and pauses are short, your flags are doing their job. Don't fiddle.

Conclusion

Aikar's flags are good defaults because they bias the JVM toward Minecraft's actual allocation pattern. They aren't magic. They're a set of reasonable choices someone made years ago, and the reasoning still holds. Knowing the reasoning lets you adapt when your workload doesn't fit the defaults.

When in doubt: G1 with Aikar's flags, heap sized to working set plus 30 percent, Xms equal to Xmx, and don't touch it unless you have telemetry telling you something is wrong.


Hosting your game server with AndroHost means we handle most of what's in this post for you automatically: tier sizing, SRV records, off-site backups, DDoS protection.

Browse plans·More posts·Discord