Minecraft Java Flags on a VPS: Aikar, GraalVM, ZGC, and What Actually Reduces Lag

By Nate Corwin · Updated 2026 · self-hosting practitioner since 2017

This article contains no affiliate links. JVM configurations are evaluated by their measured impact on server performance, not by any vendor relationship.

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:

FlagWhat it doesWhy it matters for MC
-Xms10G -Xmx10GSets min and max heap equalPrevents heap resize churn. G1GC adapts region allocation better with a fixed heap size.
-XX:+UseG1GCEnables G1 garbage collectorDesigned for large heaps with predictable pause times. Not the default on older Java versions.
-XX:+ParallelRefProcEnabledMulti-threaded weak reference processingPlugins that use weak references (many do) create work during GC. Parallelizing it shortens pause duration.
-XX:MaxGCPauseMillis=200Target max GC pause of 200msA goal, not a guarantee. G1GC adjusts young gen size to try to stay under this target. Four ticks at worst.
-XX:+DisableExplicitGCBlocks System.gc() callsPrevents poorly written plugins from triggering full-heap GC lag spikes by calling garbage collection manually.
-XX:+AlwaysPreTouchPre-allocates memory pages at startupEliminates page faults at runtime. Startup takes longer, but no lag spikes from lazy allocation during play.
-XX:G1NewSizePercent=30Minimum 30% of heap for young generationDefault is 5%, which is catastrophically small for Minecraft's allocation rate. This single flag does most of the work.
-XX:G1MaxNewSizePercent=40Maximum 40% for young generationGives G1GC room to expand the young gen during allocation bursts (chunk loading, explosions, entity spawning).
-XX:G1HeapRegionSize=8MRegion size 8MBRaises 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=20Reserves 20% as free spacePrevents "to-space exhaustion" during collection. Minecraft's allocation rate is high enough to fill regions mid-GC.
-XX:InitiatingHeapOccupancyPercent=15Start concurrent marking at 15% heap occupancyAggressive. 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=32Small survivor spaceSince MaxTenuringThreshold=1, survivor spaces are barely used. Shrinking them gives more room to eden (allocation space).
-XX:MaxTenuringThreshold=1Promote after 1 survivor cycleObjects that survive one GC are likely chunk data or player state. Promoting them immediately avoids repeated copying.
-XX:+PerfDisableSharedMemDisables shared perf memory filePrevents 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:

FlagUnder 12GBOver 12GBWhy
G1NewSizePercent3040More absolute memory available, so a larger young gen percentage is safe.
G1MaxNewSizePercent4050Same reasoning. 50% of 16GB is 8GB of young gen, enough for heavy modpacks.
G1HeapRegionSize8M16MFewer, larger regions means faster remarking. Raises humongous threshold to 8MB.
G1ReservePercent201515% of 16GB is 2.4GB. The absolute reserve is already large enough to prevent to-space exhaustion.
InitiatingHeapOccupancyPercent1520With 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:

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:

MetricG1GC (Aikar's)ZGC (Generational)
Average MSPT35ms33ms
GC pause (p99)50-100msUnder 1ms
Memory overheadBaseline+15-30%
CPU overheadBaseline+5-10%
Minimum viable setup2 cores, 4GB4 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.

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 heap12GB+ heap
2-4 CPU cores4+ cores (ideally 8+)
Vanilla or lightly modded, under 30 playersHeavy modpacks, 50+ players
Need proven reliabilityStutter 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 RAMMax heap (-Xmx)OS + native reserve
2GB1GB1GB (tight, vanilla only)
4GB2.5-3GB1-1.5GB
8GB6-6.5GB1.5-2GB
16GB12-13GB3-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:

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:

  1. 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.
  2. Entity overload. Hundreds of villagers, dropped items, or uncontrolled mob spawning. Check with /spark tickmonitor and reduce spawn-limits in bukkit.yml.
  3. 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.
  4. Chunk loading. Paper's async chunk loading helps enormously. Reduce simulation-distance to 4 and view-distance to 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

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.