Minecraft Java Flags on a VPS: Aikar, GraalVM, ZGC, and What Actually Reduces Lag
You copy Aikar's flags from a guide, paste them into your start script, and your Minecraft server still lags every 90 seconds with a 200ms spike. Or you read that GraalVM "makes Minecraft 20% faster" and spend an afternoon switching JVMs for gains you cannot measure. The Java flag ecosystem for Minecraft is a maze of cargo-culted configurations, outdated advice, and flags that do nothing on modern JVMs. This guide explains what each flag actually does, when GraalVM or ZGC beats standard G1GC, and why the most common flag mistake is allocating too much RAM.
Aikar's flags explained
Aikar's G1GC flag set is the default recommendation across PaperMC's official docs, server hosting panels, and community guides. It exists because Minecraft's memory allocation pattern is unusual: the game generates enormous quantities of short-lived objects every tick (block updates, entity positions, packet serialization buffers) and a smaller set of long-lived objects (loaded chunks, player data, world state). The default G1GC settings allocate only 5% of the heap to the young generation, which is far too small. That 5% fills within milliseconds, triggering constant minor GC pauses that manifest as rubber-banding.
Aikar's core insight was expanding the young generation to 30-40% of the heap and setting MaxTenuringThreshold=1 so objects that survive one GC cycle get promoted immediately to old generation rather than being copied between survivor spaces repeatedly. This matches Minecraft's allocation reality: an object either dies within one tick (short-lived) or lives for minutes to hours (chunk data). There is almost nothing in between.
Here is what each flag controls:
| Flag | What it does | Why it matters for MC |
|---|---|---|
-Xms10G -Xmx10G | Sets min and max heap equal | Prevents heap resize churn. G1GC adapts region allocation better with a fixed heap size. |
-XX:+UseG1GC | Enables G1 garbage collector | Designed for large heaps with predictable pause times. Not the default on older Java versions. |
-XX:+ParallelRefProcEnabled | Multi-threaded weak reference processing | Plugins that use weak references (many do) create work during GC. Parallelizing it shortens pause duration. |
-XX:MaxGCPauseMillis=200 | Target max GC pause of 200ms | A goal, not a guarantee. G1GC adjusts young gen size to try to stay under this target. Four ticks at worst. |
-XX:+DisableExplicitGC | Blocks System.gc() calls | Prevents poorly written plugins from triggering full-heap GC lag spikes by calling garbage collection manually. |
-XX:+AlwaysPreTouch | Pre-allocates memory pages at startup | Eliminates page faults at runtime. Startup takes longer, but no lag spikes from lazy allocation during play. |
-XX:G1NewSizePercent=30 | Minimum 30% of heap for young generation | Default is 5%, which is catastrophically small for Minecraft's allocation rate. This single flag does most of the work. |
-XX:G1MaxNewSizePercent=40 | Maximum 40% for young generation | Gives G1GC room to expand the young gen during allocation bursts (chunk loading, explosions, entity spawning). |
-XX:G1HeapRegionSize=8M | Region size 8MB | Raises the humongous allocation threshold to 4MB. Without this, any allocation over 2MB (half the default 4MB region) bypasses normal GC and causes fragmentation. |
-XX:G1ReservePercent=20 | Reserves 20% as free space | Prevents "to-space exhaustion" during collection. Minecraft's allocation rate is high enough to fill regions mid-GC. |
-XX:InitiatingHeapOccupancyPercent=15 | Start concurrent marking at 15% heap occupancy | Aggressive. Begins cleanup early rather than waiting for heap pressure. Prevents the old generation from filling up and causing a full GC (the worst kind of pause). |
-XX:SurvivorRatio=32 | Small survivor space | Since MaxTenuringThreshold=1, survivor spaces are barely used. Shrinking them gives more room to eden (allocation space). |
-XX:MaxTenuringThreshold=1 | Promote after 1 survivor cycle | Objects that survive one GC are likely chunk data or player state. Promoting them immediately avoids repeated copying. |
-XX:+PerfDisableSharedMem | Disables shared perf memory file | Prevents filesystem I/O during GC that causes latency on Linux under disk pressure. Breaks jstat monitoring. |
Source: PaperMC official documentation and Aikar's original blog post.
The 12GB heap threshold
Aikar documented a different flag set for heaps above 12GB. Five values change:
| Flag | Under 12GB | Over 12GB | Why |
|---|---|---|---|
G1NewSizePercent | 30 | 40 | More absolute memory available, so a larger young gen percentage is safe. |
G1MaxNewSizePercent | 40 | 50 | Same reasoning. 50% of 16GB is 8GB of young gen, enough for heavy modpacks. |
G1HeapRegionSize | 8M | 16M | Fewer, larger regions means faster remarking. Raises humongous threshold to 8MB. |
G1ReservePercent | 20 | 15 | 15% of 16GB is 2.4GB. The absolute reserve is already large enough to prevent to-space exhaustion. |
InitiatingHeapOccupancyPercent | 15 | 20 | With more total heap, old gen fills slower. Starting concurrent mark at 20% instead of 15% reduces unnecessary GC work. |
The principle: percentages that are appropriate for a 6GB heap produce different absolute values at 16GB or 24GB. Aikar's large-heap variant adjusts the ratios so the absolute amounts of young gen, reserve, and old gen remain in the right proportions.
GraalVM: when it helps and when it doesn't
GraalVM replaces HotSpot's C2 JIT compiler with the Graal compiler, which performs more aggressive inlining, escape analysis, and speculative optimization. For Minecraft, this means the hot loops that run every tick (lighting calculations, pathfinding, entity ticking) get compiled to tighter native code.
The brucethemoose benchmark project reports roughly 20% faster chunk generation on GraalVM and "modest" overall TPS gains. The chunk generation improvement is the most measurable because it is CPU-bound and exercises exactly the kind of tight loops where Graal's compiler shines. Day-to-day TPS on a server that is not chunk-generation-bottlenecked sees smaller gains, in the 5-10% range.
The trade-offs are real:
- Higher memory usage. The recommended GraalVM flag set from Obydux sets
ReservedCodeCacheSize=400Mversus HotSpot's default 250MB. Total native memory overhead is measurably higher. - Slow warmup. GraalVM CE takes significantly longer to reach steady-state performance. A GraalVM issue report documented 5x longer JIT warmup compared to HotSpot C2. The first few minutes after server start feel sluggish as the compiler optimizes hot paths.
- Oracle GraalVM vs CE. Oracle GraalVM (formerly Enterprise Edition, now free) includes the
enterprisecompiler configuration with better inlining and vectorization. GraalVM CE uses thecommunityconfiguration. The enterprise build is measurably faster, but requires accepting Oracle's license terms. - Must use G1GC. GraalVM does not support ZGC or Shenandoah. If you want a low-pause collector, you cannot use GraalVM.
- Compatibility. Most plugins and mods work fine. Cobblemon had a documented crash requiring a specific fix. Mods using Nashorn (JavaScript engine) may conflict with GraalJS. Generally not a problem, but test before committing.
The honest assessment: for a server operator who is not already maxing out their CPU, the complexity of maintaining a separate JVM, learning a different flag set, and troubleshooting GraalVM-specific issues is not justified by a 5-10% TPS improvement. GraalVM makes the most sense for large servers (50+ players, heavy modpacks) that are CPU-bottlenecked at 20 TPS and need every optimization available.
ZGC vs G1GC: the stutter question
G1GC is a stop-the-world collector. When it runs, all application threads pause. Aikar's flags target 200ms maximum pause, but real-world p99 pauses hit 50-100ms. A 100ms pause is two full ticks of rubber-banding visible to players.
ZGC performs garbage collection concurrently with application threads. Pauses are consistently under 1ms regardless of heap size. Where G1GC produces periodic spikes every 60-120 seconds, ZGC produces a flat line. Players on a ZGC server do not experience the rhythmic stutter that characterizes G1GC servers under memory pressure.
Community benchmarks show the trade-off:
| Metric | G1GC (Aikar's) | ZGC (Generational) |
|---|---|---|
| Average MSPT | 35ms | 33ms |
| GC pause (p99) | 50-100ms | Under 1ms |
| Memory overhead | Baseline | +15-30% |
| CPU overhead | Baseline | +5-10% |
| Minimum viable setup | 2 cores, 4GB | 4 cores, 8GB |
ZGC's memory overhead comes from its colored pointer implementation, which multi-maps the same physical memory to multiple virtual addresses on Linux. Tools like top report RSS inflated by roughly 3x. This is a reporting artifact, not actual physical memory usage. Check PSS (cat /proc/PID/smaps_rollup) for the real number. The actual overhead is 15-30% above G1GC's usage.
Java version matters
ZGC before Java 21 was non-generational: it scanned the entire heap every collection cycle, making it far less efficient. Java 21 introduced Generational ZGC, which divides objects by age and collects young objects more frequently. This brought ZGC's efficiency close to G1GC while maintaining sub-millisecond pauses.
- Java 21-24: Enable with
-XX:+UseZGC -XX:+ZGenerational - Java 25+: Generational mode is the default.
-XX:+ZGenerationalis no longer needed.
Do not use ZGC on Java 17. The non-generational implementation wastes CPU scanning long-lived chunk data that does not need collection.
When to use which
| Use G1GC (Aikar's flags) | Use ZGC |
|---|---|
| Under 12GB heap | 12GB+ heap |
| 2-4 CPU cores | 4+ cores (ideally 8+) |
| Vanilla or lightly modded, under 30 players | Heavy modpacks, 50+ players |
| Need proven reliability | Stutter elimination is the priority |
| Running GraalVM (ZGC not supported) | Running standard HotSpot or Adoptium |
What about Shenandoah?
Shenandoah is another concurrent, low-pause collector available in Adoptium, Amazon Corretto, and Red Hat OpenJDK. The brucethemoose benchmarks found that Shenandoah "kills server throughput" while performing well on clients. For Minecraft servers specifically, ZGC is the preferred low-pause alternative. Use Shenandoah only on the client side where its responsiveness benefits the rendering pipeline without needing the throughput that servers require.
The heap allocation mistake
The most common performance-destroying mistake on VPS Minecraft servers is allocating too much RAM to the Java heap.
When G1GC runs a mixed collection, it must scan heap regions to identify live objects. A 20GB heap has more regions to scan than an 8GB heap, even if 16GB of that 20GB is empty. The scanning work during GC pauses directly translates to longer lag spikes. Giving a vanilla server 16GB when it needs 4GB means the garbage collector processes 16GB of memory every cycle for no benefit.
PaperMC's documentation states it directly: "More memory does not mean better performance above a certain point."
How much heap to allocate
Reserve 1 to 1.5GB for the operating system and Java's own native memory (thread stacks, direct byte buffers, GC data structures, JIT code cache). The rest goes to -Xmx.
| VPS RAM | Max heap (-Xmx) | OS + native reserve |
|---|---|---|
| 2GB | 1GB | 1GB (tight, vanilla only) |
| 4GB | 2.5-3GB | 1-1.5GB |
| 8GB | 6-6.5GB | 1.5-2GB |
| 16GB | 12-13GB | 3-4GB (modded servers with large code caches) |
Swap is a death sentence
Minecraft ticks at 50ms intervals. If any tick data resides in swap, the disk I/O to page it back into RAM takes milliseconds per page fault. A single swapped page during a GC scan can push a 50ms pause to 500ms. On a VPS with HDD-backed swap, a server under memory pressure enters a death spiral: GC pauses cause swapping, swapping causes longer GC pauses, longer pauses cause more memory to become stale and swappable.
Disable swap entirely (swapoff -a) or set vm.swappiness=1 in /etc/sysctl.conf. If your server needs swap to survive, it needs more RAM, not swap.
The OOM killer
On Linux VPS, when total system memory (Java heap + Java native memory + OS) exceeds available RAM, the kernel's OOM killer terminates the largest process. That process is your Minecraft server. There is no graceful shutdown: no world save, no player disconnect message, no log entry. The server vanishes. Check dmesg | grep -i oom after an unexplained crash. If you see your Java process listed, reduce -Xmx.
How to tell if your lag is GC or something else
JVM flag tuning only fixes GC-related lag. If your server lags because a plugin runs expensive SQL queries on the main thread, or because entity spawning is out of control, or because your VPS has a slow single-thread CPU, no flag set will help. You need to identify the source before picking a solution.
Spark is the standard profiler for Minecraft servers. Install it as a plugin, run it during lag (not during idle), wait 3-5 minutes, and generate a report. The report gives you two things:
- Health report: Shows GC pauses as spikes in the tick timeline. If you see periodic 100-200ms spikes every 60-120 seconds, your lag is GC-related. Fix it with flag tuning (or switch to ZGC if stutter elimination is the goal).
- Flame graph: Shows exactly which code path consumes your tick budget. If a specific plugin function takes 30-60% of tick time, the fix is configuring, replacing, or removing that plugin. No JVM flag helps here.
Do not use Timings (Paper's built-in profiler). YouHaveTrouble's optimization guide notes that Timings "has a serious performance impact on servers." Spark has negligible overhead and produces more actionable data.
The most common non-GC lag sources on VPS servers, in order:
- Slow single-thread CPU. Minecraft's main tick loop is single-threaded. A VPS with 8 shared vCores at 2.0 GHz will lag at 20 players regardless of flags. You need fast cores, not many cores.
- Entity overload. Hundreds of villagers, dropped items, or uncontrolled mob spawning. Check with
/spark tickmonitorand reducespawn-limitsinbukkit.yml. - Plugin main-thread blocking. Database queries, HTTP requests, or file I/O that plugins run synchronously instead of async. The Spark flame graph will show exactly which plugin and which method.
- Chunk loading. Paper's async chunk loading helps enormously. Reduce
simulation-distanceto 4 andview-distanceto 7-10. Simulation distance controls how far the server actively ticks (CPU-heavy). View distance controls how far clients can see (RAM and bandwidth).
Copy-pasteable flag sets
Aikar's standard (heap under 12GB, HotSpot/Adoptium)
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 \
-jar server.jar --nogui
Replace 10G with your actual allocation. Set -Xms and -Xmx to the same value.
Aikar's large heap (over 12GB, HotSpot/Adoptium)
Same as above, but change five flags:
-XX:G1NewSizePercent=40 -XX:G1MaxNewSizePercent=50 \
-XX:G1HeapRegionSize=16M -XX:G1ReservePercent=15 \
-XX:InitiatingHeapOccupancyPercent=20
ZGC (Java 21-24, HotSpot/Adoptium)
java -Xms8G -Xmx8G -XX:+UseZGC -XX:+ZGenerational \
-XX:+AlwaysPreTouch -XX:+UseStringDeduplication \
-XX:TrimNativeHeapInterval=5000 \
-jar server.jar --nogui
ZGC needs far fewer flags than G1GC because it self-tunes. Adding G1GC-specific flags (like G1NewSizePercent) to a ZGC command line does nothing or causes errors.
ZGC (Java 25+)
java -Xms8G -Xmx8G -XX:+UseZGC \
-XX:+AlwaysPreTouch -XX:+UseStringDeduplication \
-XX:+UseCompactObjectHeaders \
-XX:TrimNativeHeapInterval=5000 \
-jar server.jar --nogui
-XX:+ZGenerational is the default on Java 25. UseCompactObjectHeaders reduces object headers from 96-128 bits to 64 bits, improving cache efficiency.
GraalVM (Oracle GraalVM, G1GC)
java -Xms8G -Xmx8G --add-modules=jdk.incubator.vector \
-XX:+UseG1GC -XX:MaxGCPauseMillis=130 \
-XX:+UnlockExperimentalVMOptions \
-XX:+UnlockDiagnosticVMOptions \
-XX:+DisableExplicitGC -XX:+AlwaysPreTouch \
-XX:G1NewSizePercent=28 -XX:G1HeapRegionSize=16M \
-XX:G1ReservePercent=20 -XX:G1MixedGCCountTarget=3 \
-XX:InitiatingHeapOccupancyPercent=10 \
-XX:G1MixedGCLiveThresholdPercent=90 \
-XX:SurvivorRatio=32 -XX:MaxTenuringThreshold=1 \
-XX:+PerfDisableSharedMem \
-XX:G1SATBBufferEnqueueingThresholdPercent=30 \
-XX:G1ConcMarkStepDurationMillis=5 \
-XX:G1ConcRSHotCardLimit=16 \
-XX:G1ConcRefinementServiceIntervalMillis=150 \
-XX:G1RSetUpdatingPauseTimePercent=0 \
-XX:-DontCompileHugeMethods \
-XX:MaxNodeLimit=240000 -XX:NodeLimitFudgeFactor=8000 \
-XX:ReservedCodeCacheSize=400M \
-XX:NonNMethodCodeHeapSize=12M \
-XX:ProfiledCodeHeapSize=194M \
-XX:NonProfiledCodeHeapSize=194M \
-XX:+AlwaysActAsServerClassMachine \
-XX:+EagerJVMCI \
-Dgraal.TuneInlinerExploration=1 \
-Dgraal.LoopRotation=true \
-Dgraal.OptWriteMotion=true \
-Dgraal.CompilerConfiguration=enterprise \
-jar server.jar --nogui
For GraalVM CE (not Oracle), remove the last -Dgraal.CompilerConfiguration=enterprise line. Source: brucethemoose and Obydux.
Bare minimum safe defaults
java -Xms4G -Xmx4G -XX:+UseG1GC \
-XX:+AlwaysPreTouch -XX:+DisableExplicitGC \
-jar server.jar --nogui
If you do not want to learn what any flag does, this is the safe floor. G1GC with pre-touch and explicit GC disabled. Better than the JVM defaults, worse than Aikar's full set.
Common mistakes
- Setting -Xms lower than -Xmx. Aikar and PaperMC recommend equal values. When the heap grows from
-Xmsto-Xmxduring operation, G1GC must reorganize regions. This causes pauses that look like GC lag but are actually heap resize overhead. - Allocating all VPS RAM to Java. A 4GB VPS with
-Xmx4Gleaves zero for the OS. The OOM killer will eventually terminate your server with no warning. Reserve 1-1.5GB. - Running OpenJ9 (IBM/Eclipse). Forge and NeoForge produce an explicit error: "You are trying to run with an unsupported Java Virtual Machine: Eclipse OpenJ9 VM." Even when it runs, the brucethemoose benchmarks measured OpenJ9 as "over 30% slower at server chunkgen" than HotSpot. OpenJ9 saves RAM but costs performance that Minecraft servers cannot afford.
- Using Aikar's flags on GraalVM. The G1GC tuning parameters overlap, but some flags behave differently on GraalVM's runtime. Use the GraalVM-specific flag set. Mixing and matching causes subtle performance regressions.
- Running ZGC on a 2-core 4GB VPS. ZGC's concurrent collection needs CPU cycles that a 2-core VPS cannot spare without competing with the game tick thread. ZGC's memory overhead means less room for actual game data. G1GC with Aikar's flags will outperform ZGC on constrained hardware.
- Adding G1GC flags to a ZGC command line. Flags like
G1NewSizePercent,G1HeapRegionSize, andG1ReservePercentare G1-specific. They either get silently ignored or produce startup warnings when paired with-XX:+UseZGC. ZGC self-tunes and needs very few flags. - Ignoring swap on a VPS. Many VPS providers enable swap by default. Even 1GB of swap will ruin Minecraft performance the moment Java touches it. Check with
free -hand disable withswapoff -a. - Tuning flags before profiling. If your lag is from a plugin blocking the main thread, no JVM flag will help. Install Spark, identify the source, then decide whether flag tuning is even the right response.
FAQ
Should I use Aikar's flags or ZGC for my Minecraft server?
Use Aikar's flags (G1GC) for servers with under 12GB heap and fewer than 4 CPU cores. G1GC has lower overhead, and Aikar's tuning is the most tested Minecraft configuration. Use ZGC if you have 12GB+ heap, 4+ cores, and stutter elimination matters more than raw throughput. ZGC achieves sub-millisecond pauses compared to G1GC's 50-200ms spikes, but uses 15-30% more memory. For most VPS Minecraft servers running vanilla or lightly modded Paper with 20-30 players, Aikar's flags are the right choice.
Does GraalVM actually make Minecraft servers faster?
Modestly. Community benchmarks report roughly 20% faster chunk generation and 5-10% TPS gains on GraalVM versus HotSpot. The improvement comes from a more aggressive JIT compiler. The trade-offs: higher memory usage (400MB code cache vs 250MB default), significantly slower startup and warmup, and GraalVM does not support ZGC. Oracle GraalVM (formerly Enterprise, now free) outperforms GraalVM CE. For most server operators, the complexity is not worth the gains unless you are CPU-bottlenecked at 20 TPS with 50+ players.
How much RAM should I allocate to a Minecraft server on a VPS?
Reserve 1 to 1.5GB for the OS and Java's native memory. On a 4GB VPS, allocate 2.5 to 3GB. On 8GB, allocate 6 to 6.5GB. Over-allocating forces the garbage collector to scan unused memory, producing longer pause spikes. Set -Xms equal to -Xmx. Disable swap or set vm.swappiness=1, because swap turns a 50ms GC pause into a 500ms disk I/O pause.
Why does my Minecraft server lag even with Aikar's flags?
Aikar's flags fix GC lag only. If your lag comes from slow plugins, entity overload, or insufficient CPU speed, JVM tuning will not help. Install Spark (spark.lucko.me) and run it during lag. The flame graph shows exactly which code path consumes your tick budget. Periodic 100-200ms spikes in the health report mean GC lag. A plugin function taking 30-60% of tick time means plugin lag. Most Minecraft server lag on a VPS is CPU-bound.