The Best Aikar JVM Flags for a Minecraft Server (and When to Use ZGC)
Aikar's G1GC flags explained for Minecraft server owners — why Xms should equal Xmx, the OS headroom rule, the large-heap variant, and when ZGC beats G1GC.
For most servers the answer is short: paste Aikar's flags, set -Xms equal to -Xmx, and leave the OS a bit of room. You almost never need to touch the individual numbers, and the defaults are already the right starting point. The one case where you'd reach for something else is a large modded heap on Java 21 or newer, where the G1 garbage collector can get out-paced and a different collector called ZGC does better — but that's the exception, not the rule.
Below the paste-and-go block, each flag gets a short explanation of why it's there, so if you do end up tuning you're making a real decision instead of swapping numbers blind. This is owner territory, by the way — memory allocation is something you set on the server, not anything a player does to connect.
What Aikar's flags actually are
Aikar's flags are a pre-tuned set of JVM startup arguments built around the G1 garbage collector, usually shortened to G1GC. G1 has been Java's default collector since Java 9, and PaperMC recommends this flag set as the baseline config for most Paper servers.
They exist because Minecraft has an unusual memory pattern. Every tick the server allocates a huge number of short-lived objects — entities, packets, block updates — and then throws almost all of them away again. Stock GC settings handle that churn badly. The garbage collector falls behind, then catches up all at once in a stop-the-world pause where the whole server freezes for a moment. That's the lag spike you feel, and it's where "Can't keep up" comes from when the pause runs long.
The flags don't touch any game settings. All they change is how the JVM collects garbage — breaking those big occasional pauses into small frequent ones, so a tick rarely stalls waiting on the collector. And because they tune the collector rather than Paper itself, they apply to any G1GC Minecraft server. Spigot, Fabric, Forge — same collector underneath, same flags.
Set -Xms equal to -Xmx, and leave the OS some room
The first rule is that -Xms (the initial heap) should equal -Xmx (the max heap). Aikar's reasoning is that unused memory is wasted memory: hand the JVM the whole heap up front instead of letting it slowly grow into it. A fixed heap size also lets G1 plan its regions around a number that won't move, which it does better with.
Pair that with -XX:+AlwaysPreTouch. This forces the OS to actually map and zero every page of the heap at startup, so you pay that cost once on boot. Without it, the OS hands you those pages lazily — and "lazily" tends to mean the moment a bunch of players join and the server reaches for memory it hasn't really claimed yet. Pre-touching moves that cost off the hot path.
Now the headroom rule, which trips people up: don't allocate the whole machine. PaperMC's guidance is to keep your allocation roughly 1000 to 1500MB below the host's total RAM. The OS needs memory, the JVM has its own off-heap overhead that lives outside the heap you sized, and anything else on the box needs room too. So on an 8GB machine you allocate around 6.5GB, not 8. Set -Xmx to the full machine size and you can run the system out of memory, at which point the OS kills the process to save itself — and a server that gets killed under load is worse than one that's a little smaller. The headroom is your safety margin.
How much heap to give it in the first place is a separate question from how to allocate it, and it depends on your gamemode and player count — there's a breakdown of RAM by player count if you're sizing a box from scratch.
What each flag is doing
You don't need to memorize all of these, but knowing the intent behind a few makes the rest read as a strategy instead of noise.
-XX:+UseG1GC turns on the collector everything else tunes. -XX:+ParallelRefProcEnabled lets reference processing run across threads instead of single-file, so it isn't a bottleneck. -XX:MaxGCPauseMillis=200 sets the pause-time target — 200ms is about four ticks — telling G1 to keep collections short rather than chase maximum throughput. For a game server, short and frequent beats long and efficient.
-XX:G1NewSizePercent=30 and -XX:G1MaxNewSizePercent=40 grow the young generation well past G1's defaults. This is the load-bearing change for Minecraft: all that short-lived churn wants a big nursery, so most garbage dies young and gets cleaned up cheaply instead of surviving long enough to be promoted into old gen. -XX:MaxTenuringThreshold=1 reinforces that by stopping transient objects from getting promoted early, which keeps old-gen collections — the expensive ones — rare.
-XX:InitiatingHeapOccupancyPercent=15 starts G1's concurrent marking cycle early, at 15% occupancy, so the collector stays ahead of allocation instead of waking up late and stalling to catch up. The rest of the set — G1HeapRegionSize, G1ReservePercent, G1HeapWastePercent, G1MixedGCCountTarget, G1RSetUpdatingPauseTimePercent, SurvivorRatio — are all sized to that same early-and-incremental strategy. Two more worth calling out: -XX:+DisableExplicitGC makes the JVM ignore System.gc() calls, because some plugins and mods call it and each call forces a full stop-the-world collection you never asked for; and -XX:+PerfDisableSharedMem sidesteps a known I/O stall the JVM can hit writing its perf data file.
Here's the canonical set as a paste-and-go block. Swap the [N] values for your heap size in gigabytes:
java -Xms[N]G -Xmx[N]G -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 \
-jar paper.jar --nogui
The large-heap variant, over about 12GB
Above roughly 12GB of allocated heap, those numbers aren't ideal anymore — the standard set is tuned for smaller heaps. Aikar specifies a different group for big allocations: -XX:G1NewSizePercent=40, -XX:G1MaxNewSizePercent=50, -XX:G1HeapRegionSize=16M, -XX:G1ReservePercent=15, and -XX:InitiatingHeapOccupancyPercent=20. G1MixedGCCountTarget stays at 4.
The logic follows from having more room to work with. With a larger heap you can hand a bigger share to the young generation (40/50 instead of 30/40), a 16M region size suits the bigger total, and you can drop the reserve percent because 15% of a large heap is already plenty in absolute megabytes. Heavy modpacks running at 12 to 16GB and up should be on these numbers — leaving the small-heap defaults on a big modded heap just leaves performance on the floor.
When to use ZGC instead
The honest version first: G1GC with Aikar's flags is the right answer for the large majority of servers, and most owners reading this should stop at the previous section. ZGC only matters in one specific situation — a large heap where garbage-collection pauses are your measured bottleneck.
What ZGC brings is pause times that are sub-millisecond and, more to the point, independent of heap size. G1's pauses can stretch as the heap grows; ZGC's don't, so a 24GB modded heap doesn't pay the longer pauses G1 can show at that scale. The version that matters for Minecraft is Generational ZGC, which handles short-lived allocations efficiently — exactly the pattern a Minecraft server produces. It arrived in Java 21 as opt-in (-XX:+UseZGC -XX:+ZGenerational together) and became the default from Java 23 on, where -XX:+UseZGC alone gives you the generational version and the separate -XX:+ZGenerational flag is deprecated. Since recent Minecraft versions need Java 21 or newer anyway, this is realistically available to you now.
The catch is that ZGC is not Aikar's flags. You drop the G1-specific tuning and run a small ZGC set instead — you can't mix them. ZGC also trades a little throughput for that low latency and needs heap headroom to do its concurrent work while the server keeps allocating, so undersizing the heap hurts it more than it would hurt G1.
The decision rule is the same one that should govern any GC change: profile first. Run something like Spark and watch your TPS. If TPS is healthy, or Spark points the finger at CPU rather than memory — a runaway redstone contraption is the usual culprit — then no garbage collector is going to help, and you stay on G1. Only a large heap whose lag actually traces back to GC pauses is a candidate for ZGC.
Putting it together
The default path covers nearly everyone: paste Aikar's flags, set -Xms equal to -Xmx, leave the OS 1 to 1.5GB, and switch to the large-heap variant if you're over about 12GB. If you're running a big modded heap on Java 21 or newer and you've proven GC is the bottleneck, try Generational ZGC and re-measure to confirm it actually helped.
It's worth remembering why any of this matters for a public server. Smooth TPS and uptime that doesn't wobble are what keep people coming back, and a community that keeps showing up is the one that votes — which is what moves you up the monthly server rankings. It all comes down to measuring instead of guessing, so change one thing at a time, look at what the numbers actually do, and keep whatever helped.
FAQ
I'm running in a Docker container — does the headroom rule change?
The principle holds but the math is trickier, because two layers can each think they own the memory. Set the container's memory limit first, then size -Xms/-Xmx to sit below that limit by the usual margin, not below the physical host's RAM. If the JVM's heap plus its off-heap overhead climbs past the container limit, the container runtime kills the process with an OOM, and from inside the server it looks like a random crash with no Java stack trace. On modern JDKs the JVM reads the cgroup limit rather than the host's total, so a forgotten -Xmx defaults against the container size instead of the whole machine — but pin it explicitly anyway so you control the number.
Do the standard flags still apply if I'm running a Velocity or BungeeCord proxy?
No — leave them off the proxy. Aikar's flags are tuned for the allocation churn of a world-ticking game server, and a proxy doesn't tick a world or hold one in memory; it shuffles packets between backends. A proxy needs only a small heap and does fine on the JVM defaults, so the big young-generation sizing and the rest of the G1 tuning are solving a problem it doesn't have. Save the flag block for the backend Paper, Fabric, or Forge instances that actually run the worlds.
I upgraded my JDK and now I see "Unrecognized VM option" on boot — what broke?
A flag in your block was renamed, removed, or made a no-op in the newer Java release, and the JVM refuses to start rather than guess. The fix is to pull a fresh copy of the flags generated for your current Java version instead of carrying an old startup line forward across a major JDK jump. One specific trap: -XX:+UnlockExperimentalVMOptions is in the standard block to enable options that were experimental on older Java, and if a later release graduates one of those to a normal option, the unlock can start complaining. Regenerate, don't patch by hand.
Does a higher -XX:MaxGCPauseMillis give me better performance since it allows longer pauses?
It's the opposite of a performance dial, and raising it is usually a mistake for a game server. The number is a target you're asking G1 to hit, and a higher target tells the collector it's allowed to pause longer to do more work per cycle — which trades exactly the thing you care about (short, predictable ticks) for raw throughput you don't need. The default 200ms is already roughly four ticks, which is about as long a freeze as a server can take without players feeling it. If anything you'd lower it, not raise it, though G1 can only honor the target so far before it just collects more often instead.


