Your garbage collector choice is the single biggest JVM decision you’ll make for a Minecraft server. Here’s why.
Minecraft’s server thread runs a game loop at 20 ticks per second. Each tick has a 50ms budget. When the JVM triggers a stop-the-world GC pause, the server thread freezes. A 200ms pause means 4 missed ticks. Players see entities rubber-band, blocks snap back, and redstone circuits skip beats. A 400ms pause (8 ticks) is plainly visible to everyone online.
The right collector depends on three things: how much heap you’re allocating, which Java version you’re running, and how many CPU cores you have. This guide covers the three collectors that matter in 2026, explains when each one wins, and gives you copy-paste flag sets. If you haven’t confirmed GC is actually your bottleneck yet, check /spark gc first; our guide to reading a Spark profiler report covers how to tell GC pressure apart from other lag sources.
G1GC: The Proven Default
G1GC (Garbage-First Garbage Collector) has been the JVM default since Java 9, replacing the throughput-oriented Parallel GC. It divides the heap into equally-sized regions and categorizes them as eden, survivor, or old generation. During a young collection, G1 evacuates live objects from eden and survivor regions into new regions, reclaiming the originals instantly.
The key tuning knob is -XX:MaxGCPauseMillis (default: 200ms). G1 uses this as a soft target, adjusting how many regions it collects per cycle to stay near that goal. It does not guarantee the target, but it actively works toward it.
The Humongous Object Problem
Any object larger than half a region’s size is classified as “humongous” and allocated directly into old generation regions. In Minecraft, chunk loading and world generation can produce these large allocations. Humongous objects skip the young generation entirely and only get collected during expensive old-gen cycles. Java 21 improved this by allowing G1 to move humongous objects during full GC and parallelize TLAB setup, reducing full GC pause times.
Aikar’s Flags: G1GC Tuned for Minecraft
Aikar’s flags are the community standard for G1GC tuning on Minecraft servers. They exist because Minecraft’s allocation pattern is unusual: the server allocates heavily every tick (entity updates, chunk processing, packet serialization), and most of those objects die within one GC cycle.
The critical flags and what they do:
-XX:G1NewSizePercent=30/-XX:G1MaxNewSizePercent=40: Reserves 30-40% of the heap for the young generation. Default G1 dynamically sizes young gen down to ~5%, which causes more frequent, less efficient collections for Minecraft’s high allocation rate.-XX:InitiatingHeapOccupancyPercent=15: Starts concurrent marking when old gen is only 15% full (default is 45%). This triggers mixed GC cycles early, preventing the heap from filling up and forcing a full STW collection.-XX:MaxTenuringThreshold=1: Promotes objects to old gen after just 1 survivor cycle instead of the default 15. For Minecraft, most objects either die in eden or live forever (cached chunks, loaded entities). Keeping them in survivor space for 15 cycles wastes time.-XX:SurvivorRatio=32: Shrinks survivor space in favor of a larger eden. WithMaxTenuringThreshold=1, objects spend almost no time in survivor space, so it doesn’t need much room.
For a deeper walkthrough of each flag and the reasoning behind it, see our Aikar’s Flags Explained post.
When to Use G1GC
G1GC is the right choice for most Minecraft servers. It works on any hardware, any heap size from 2 GB to 16 GB, and delivers predictable results. With Aikar’s flags applied, expect average pause times in the 20-40ms range and p99 pauses under 200ms on a properly sized heap.
G1GC improved dramatically from Java 8 through 17, and saw further incremental gains in Java 21. If you’re still on Java 8 or 11, upgrading to 21 with Aikar’s flags is likely the single largest performance improvement available to you.
Best for: 4-12 GB heap, any hardware, any experience level.
ZGC: Sub-Millisecond Pauses
ZGC takes a fundamentally different approach. Instead of stopping the application to move objects, ZGC performs nearly all of its work concurrently with the application threads. It uses colored pointers (metadata stored in unused bits of 64-bit object references) and load barriers (small code snippets injected at every object reference load) to track and relocate objects while the server keeps running.
The result: GC pauses measured in microseconds, not milliseconds. And those pauses stay constant regardless of heap size. A 4 GB heap and a 128 GB heap produce the same pause times.
Generational ZGC: The Critical Distinction
If you’re evaluating ZGC in 2026, you need generational mode. Here’s the history:
- Java 15-20: ZGC was non-generational only. It treated all objects the same age, which meant the collector had to scan the entire heap every cycle. For Minecraft’s high short-lived allocation rate, this caused allocation stalls under load.
- Java 21: Generational ZGC was introduced via
-XX:+ZGenerational. It separates objects into young and old generations, just like G1, so short-lived allocations can be collected cheaply without scanning the full heap. This was the change that made ZGC practical for Minecraft. - Java 23+: Generational mode became the default. Non-generational ZGC was deprecated.
- Java 24+: Non-generational ZGC is removed entirely. ZGC is always generational.
On Java 21 or 22, you must explicitly enable it: -XX:+UseZGC -XX:+ZGenerational. On Java 23+, -XX:+UseZGC alone gives you generational mode.
For more on the design, see the Inside Java Generational ZGC explainer and the OpenJDK ZGC project page.
The Trade-offs
ZGC is not free. You’re trading memory and CPU for pause-time elimination:
- No compressed oops: ZGC uses 64-bit object references (colored pointers require the extra bits). G1GC with compressed oops uses 32-bit references for heaps under 32 GB. This means ZGC’s memory footprint is roughly 1.5x the live data set, compared to ~1.15x for G1GC. A server that runs well on 8 GB with G1 may need 12-16 GB with ZGC to hold the same world.
- CPU overhead: ZGC’s concurrent threads consume CPU cycles continuously, roughly 5-8% of total CPU, compared to G1’s bursty STW model. You need spare cores for this. On a 2-core VPS, ZGC’s background threads will compete with the server thread.
Benchmark: G1GC vs Generational ZGC
The MineGuard benchmark tested G1GC against Generational ZGC under controlled conditions:
Test environment: Paper 1.21.4, Java 21.0.5 (Temurin), AMD Ryzen 9 5950X, 64 GB DDR4, 200+ players online with active mob farms and Chunky chunk pre-generation, 3-hour sustained load per collector.
| Metric | G1GC (8 GB heap) | Gen ZGC (16 GB heap) |
|---|---|---|
| Average GC pause | 28ms | 0.18ms |
| P99 GC pause | 145ms | 0.9ms |
| Max GC pause | 312ms | 2.1ms |
| Lowest TPS | 18.4 | 19.6 |
Note the heap difference: ZGC needed 16 GB to match what G1 did in 8 GB, confirming the memory overhead. But the pause-time improvement is dramatic. G1’s 312ms worst case is 6 missed ticks. ZGC’s 2.1ms worst case is imperceptible.
Important: Do Not Mix Aikar’s Flags with ZGC
Aikar’s flags are G1GC-specific. Flags like G1NewSizePercent, G1MaxNewSizePercent, and InitiatingHeapOccupancyPercent are G1 internals that ZGC ignores or, worse, that cause confusing warning messages. If you switch to ZGC, start with a clean flag set.
Best for: 12+ GB heap, 4+ real CPU cores, Java 21+.
Shenandoah: The Cautionary Tale
Shenandoah, developed by Red Hat, is another concurrent low-pause collector. It uses Brooks forwarding pointers (an extra word before each object that points to the object’s current location) to enable concurrent compaction. This means Shenandoah can move objects while the application is running, similar to ZGC but with a different mechanism.
Red Hat’s own benchmarks on Java 17 show initial-mark pauses as low as 63 microseconds. On paper, that’s excellent.
Why It Doesn’t Work for Minecraft Servers
There are three problems:
1. Non-generational design. Shenandoah does not separate young and old objects. Every collection cycle must consider the entire heap. For Minecraft, which generates enormous volumes of short-lived objects every tick, this is deeply inefficient. A generational Shenandoah mode was developed but has not landed in mainline OpenJDK.
brucethemoose’s benchmarks tested Shenandoah against G1GC and ZGC in Minecraft-specific workloads. The conclusion: “Shenandoah performs well on clients, but kills server throughput in my tests.” Without a young generation to cheaply collect tick-scoped allocations, Shenandoah’s concurrent threads burn excessive CPU scanning the full heap.
2. Availability. Shenandoah is not included in Oracle JDK builds. You need an OpenJDK distribution that includes it, such as Eclipse Temurin or Amazon Corretto. This limits deployment flexibility and can create confusion when switching between JDK vendors.
3. Community verdict. The PaperMC Velocity tuning guide states that the Velocity team “is not aware of any successful deployments of Shenandoah with Velocity in the wild.” When the largest Paper-family project effectively warns against it, that’s a strong signal.
Best for: Client-side JVMs and non-Minecraft workloads. Not recommended for Minecraft servers.
Parallel and Serial GC: Don’t
Two other collectors ship with the JVM. Neither belongs on a Minecraft server.
Parallel GC (formerly the default before Java 9) is a throughput-maximizing collector. It stops the entire application for every collection, using multiple threads to finish quickly. “Quickly” is relative: pauses can reach multiple seconds on large heaps. The PaperMC Velocity documentation warns that Parallel GC’s “pause times tend to be long, and are not suitable for Minecraft.”
Serial GC is a single-threaded stop-the-world collector designed for embedded systems and JVMs with tiny heaps (under 100 MB). If you see -XX:+UseSerialGC in your startup script, something has gone wrong.
Java Version Matters
Your Java version determines which collectors are available and how well they perform.
| Minecraft Version | Minimum Java | Notes |
|---|---|---|
| 1.17 | Java 16 | Java 21 works and is recommended |
| 1.18 - 1.20.4 | Java 17 | Java 21 works and is recommended |
| 1.20.5 - 1.21.x | Java 21 | Required, not optional |
| 1.22+ | Java 21 | Check server requirements for updates |
Key version milestones for GC:
- Java 17: G1GC is mature and well-tuned. ZGC available but non-generational only (not recommended for MC).
- Java 21 (LTS): G1GC gets humongous object improvements. Generational ZGC available via
-XX:+ZGenerational. This is the sweet spot for 2026. - Java 24+: ZGC is generational-only (non-generational removed via JEP 490).
Our recommendation for 2026: Java 21 LTS as the baseline. It’s required by modern Minecraft versions, has the best GC options, and will receive security updates through at least 2029.
Decision Matrix
| Heap Size | Java Version | Hardware | Recommended GC |
|---|---|---|---|
| Under 4 GB | Any | Any | G1GC with Aikar’s flags (but consider if the server needs more RAM) |
| 4 - 12 GB | 21 | Any | G1GC with Aikar’s flags |
| 12 - 16 GB | 21 | 4+ cores, 16+ GB system RAM | Benchmark both G1GC and Generational ZGC |
| 16+ GB | 21 | 6+ cores | Generational ZGC |
| Any | 23+ | Any | Generational ZGC (default) |
The 12-16 GB range is genuinely ambiguous. G1GC with Aikar’s flags still performs well here, and uses less total memory. ZGC eliminates tail-latency spikes but needs the extra headroom. If your server regularly hits 100+ players or runs heavy modpacks (ATM9, RLCraft), the ZGC trade-off is usually worth it. For vanilla or lightly modded servers, G1GC is fine.
Quick-Start Commands
G1GC with Aikar’s Flags (10 GB)
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:G1MixedGCCountTarget=4 \
-XX:InitiatingHeapOccupancyPercent=15 \
-XX:G1MixedGCLiveThresholdPercent=90 \
-XX:G1RSetUpdatingPauseTimePercent=5 \
-XX:SurvivorRatio=32 \
-XX:+PerfDisableSharedMem \
-XX:MaxTenuringThreshold=1 \
-jar server.jar --nogui
Generational ZGC (16 GB)
java -Xms16G -Xmx16G \
-XX:+UseZGC \
-XX:+ZGenerational \
-XX:+AlwaysPreTouch \
-XX:+DisableExplicitGC \
-XX:+PerfDisableSharedMem \
-jar server.jar --nogui
ZGC needs far fewer flags because it self-tunes aggressively. The flags above are sufficient for most deployments. Do not add G1-specific flags.
A Note on Xms = Xmx
Both examples set -Xms equal to -Xmx. This is intentional. When Xms is lower than Xmx, the JVM starts with a small heap and grows it on demand. Each growth event triggers a GC pause. On a Minecraft server, that pause freezes every player. Pre-allocating the full heap at startup eliminates resize pauses entirely. Our analysis of 7,661 real server configs found that 15% of servers still make this mistake.
How We Handle This at Zerve
Every Zerve server ships with the optimal GC configuration for its plan size. Servers on 4-12 GB plans get G1GC with Aikar’s flags. Larger plans get Generational ZGC with tuned concurrent thread counts matched to their dedicated CPU cores. You don’t need to paste flags into a startup script or guess which collector fits your workload.
Our Spark Analyzer can also diagnose GC issues on any server, not just ours. Paste a Spark profile link and it will flag misconfigured collectors, missing Aikar’s flags, and Xms/Xmx mismatches automatically.
Sources
- Aikar’s Flags (PaperMC documentation)
- MineGuard: ZGC vs G1GC on Java 21 benchmark
- brucethemoose: Minecraft Performance Flags Benchmarks
- Obydux: Minecraft startup flags
- Oracle: Z Garbage Collector tuning guide
- OpenJDK: ZGC project
- JEP 439: Generational ZGC
- Inside Java: Generational ZGC explainer
- Red Hat: Shenandoah in OpenJDK 17, sub-millisecond GC pauses
- PaperMC: Velocity tuning
- Paper Chan: Paper optimization guide
- JDK 21: G1 and Parallel GC changes
- Minecraft Wiki: Server requirements