Two decisions about the JVM affect a Minecraft server more than every other tuning knob combined: which Java version you run, and how much heap you give it. The first has one right answer per Minecraft version and no room for opinion. The second is where people go wrong in both directions, giving a 2 GB container a 2 GB heap and wondering why it dies, or giving a twenty-player server 16 GB and wondering why the pauses got worse.
Everything after those two is garbage collector tuning, which is worth a real but modest improvement - smoother tick times rather than a higher ceiling. Aikar's flags are the sensible default for that, they have been for years, and this post explains what each one actually does so you can decide whether to keep it.
Which Java version for which Minecraft version#
Minecraft's server is compiled for a specific Java release. Newer Java runs older Minecraft in almost every case; older Java never runs newer Minecraft.
| Minecraft | Minimum Java | In practice |
|---|---|---|
| 1.8 - 1.16.5 | Java 8 | Java 8 or 11. Newer Java breaks some old plugins |
| 1.17 - 1.17.1 | Java 16 | Java 17 works and is easier to find |
| 1.18 - 1.20.4 | Java 17 | Java 21 also works for Paper |
| 1.20.5 and newer | Java 21 | Including all of 1.21 |
The failure mode when you get it wrong is unmistakable:
Error: LinkageError occurred while loading main class io.papermc.paperclip.Mainjava.lang.UnsupportedClassVersionError: io/papermc/paperclip/Main has beencompiled by a more recent version of the Java Runtime (class file version 65.0),this version of the Java Runtime only recognizes class file versions up to 61.0Class file 65 is Java 21, 61 is Java 17, 60 is Java 16, 55 is Java 11, 52 is Java 8. Read the two numbers, install the higher one, done.
The other direction is subtler. Running 1.12.2 on Java 17 or 21 often works and sometimes does not, because old plugins and old library versions use reflection into JDK internals that later releases closed off. If you maintain a legacy server, stay on the Java the game shipped with. If you run a modpack, the pack's own documentation states a Java version and you should believe it - modded Minecraft without the crashes covers the rest of that particular minefield.
On RE:NODE the Java version is picked to match the Minecraft version when the server is created, so a fresh 1.21 server is already on Java 21. If you upload a different jar yourself, check the Startup tab before wondering why it will not boot.
Which Java build to install#
Any OpenJDK build of the right version will run a Minecraft server correctly. The differences are packaging and support, not performance.
- Eclipse Temurin from Adoptium is the usual default: free, well-tested, available for every LTS version.
- Amazon Corretto is the other common choice, with long support windows.
- Microsoft Build of OpenJDK and Azul Zulu are equally fine.
- Oracle JDK works but carries a licence you should read before deploying it commercially.
Take the long-term-support releases - 8, 11, 17, 21 - rather than a six-month interim release, because plugin authors test against LTS. On Linux, install the headless package (openjdk-21-jre-headless on Debian and Ubuntu) so you are not pulling in graphics libraries a server never loads.
GraalVM comes up periodically as a performance win. The results on Minecraft are inconsistent: some workloads gain a few percent, some lose. It is not a mistake, but it is not the free upgrade it is sometimes described as, and you should benchmark your own server before committing.
Heap size, and the gap between -Xmx and the plan#
-Xms sets the starting heap, -Xmx the maximum. Set them to the same value. The JVM cannot usefully hand memory back to a fixed-size container, resizing the heap costs pauses, and an equal pair means the memory graph tells you something rather than showing a sawtooth.
The part people miss is that -Xmx is not the server's memory usage. It is the heap. On top of it the JVM needs:
- Metaspace, for loaded classes. A plugin-heavy server can hold 150-300 MB here, and a modpack considerably more.
- Code cache, for JIT-compiled methods. Tens of megabytes, growing with uptime.
- Thread stacks, roughly 1 MB each, with dozens of threads.
- Direct byte buffers, used by Netty for networking. Off-heap and invisible to
-Xmx. - The GC's own structures, which for G1 are a meaningful percentage of the heap.
So the total resident memory of a Minecraft server is comfortably -Xmx plus 400 MB to 1 GB, more on a modded server. If that total crosses the container limit, the kernel stops the process, and there is no Java exception, no crash report and no clue in the log except that it ends mid-line.
| Plan memory | Set -Xmx to | Headroom left |
|---|---|---|
| 2 GB | 1536M | 512 MB |
| 4 GB | 3G | 1 GB |
| 6 GB | 5G | 1 GB |
| 10 GB | 8G | 2 GB |
| 14 GB | 12G | 2 GB |
Those are deliberately conservative. Start there, watch the memory graph for a week under real load, and raise -Xmx if the heap is genuinely the constraint. Raising it because there is unused memory in the graph is how you turn a slow server into a restarting one.
The related mistake is believing that a larger heap is a faster server. It is not. Java does not use memory it has not been asked for, and every G1 collection has to scan and copy live objects, so a bigger heap means more work per collection cycle and longer worst-case pauses. A twenty-player Paper server on 6 GB with sensible flags usually ticks better than the same server on 16 GB. How much RAM a Minecraft server needs has the numbers by player count, and node memory limits explained makes the same argument for the app side.
Aikar's flags, line by line#
The canonical flag set for Minecraft on G1 has been stable for years. For a heap under 12 GB:
$ java -Xms5G -Xmx5G \ -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 --noguiFor a heap of 12 GB or more, five values change: G1NewSizePercent=40, G1MaxNewSizePercent=50, G1HeapRegionSize=16M, G1ReservePercent=15, InitiatingHeapOccupancyPercent=20. Everything else stays.
What they do:
UseG1GCselects the garbage collector. It is already the default on Java 9 and later, but stating it means a changed default cannot surprise you.ParallelRefProcEnabledprocesses weak and soft references in parallel during a pause. Minecraft creates a great many of them, and this is one of the clearer wins in the set.MaxGCPauseMillis=200is a target, not a promise. G1 sizes its work to try to stay under it. Note what 200 ms means to a game with a 50 ms tick: a pause at the target skips four ticks. Lowering it to 50 does not give you 50 ms pauses, it gives you far more frequent collections.UnlockExperimentalVMOptionsis required because several of the G1 options below were marked experimental.DisableExplicitGCmakes the JVM ignoreSystem.gc()calls. Plugins and libraries occasionally call it, and every call is a full stop-the-world collection you did not need.AlwaysPreTouchwrites to every page of the heap at startup so the memory is committed up front, avoiding page faults during play. The trade is a slower start and a memory graph that sits at-Xmxfrom the first second, which makes the panel graph useless as a "how much is in use" indicator.G1NewSizePercent=30andG1MaxNewSizePercent=40grow the young generation well beyond G1's default of 5%. Minecraft allocates enormously and most of it dies within a tick or two, so a large eden means fewer collections and far less promotion into the old generation.G1HeapRegionSize=8Menlarges G1's regions. Objects larger than half a region are "humongous" and allocated specially, which is slow; Minecraft's chunk and palette arrays are large enough to hit that with the default region size.G1ReservePercent=20keeps a slice of the heap free so that an evacuation always has somewhere to go. Without it you get to-space exhaustion, which degrades into a full collection.G1HeapWastePercent=5andG1MixedGCCountTarget=4make mixed collections start sooner and finish in fewer, larger steps.InitiatingHeapOccupancyPercent=15starts the concurrent marking cycle at 15% heap occupancy instead of the default 45%. Combined with the large young generation, this keeps the old generation clean and avoids full collections entirely in normal operation.G1MixedGCLiveThresholdPercent=90lets mixed collections reclaim regions that are up to 90% live, so nothing is left permanently uncollected.G1RSetUpdatingPauseTimePercent=5moves remembered-set maintenance out of the pause and into concurrent time.SurvivorRatio=32andMaxTenuringThreshold=1together mean objects are either collected immediately or promoted after surviving once, rather than being copied back and forth between survivor spaces. For Minecraft's allocation pattern, copying is wasted work.PerfDisableSharedMemstops the JVM writing performance counters to a memory-mapped file under/tmp. On a busy or slow disk that write can stall the whole JVM, and nothing you use needs the file.
You will also see -Dusing.aikars.flags=https://mcflags.emc.gs and -Daikars.new.flags=true in the published version. They are markers with no effect, there so that someone reading a crash report can see the flags were applied.
One optional extra on Java 17 and later: --add-modules=jdk.incubator.vector lets Paper use the incubating Vector API for some of its maths. The JVM prints a warning about using an incubator module on startup, which is expected and not an error.
Collectors: G1, ZGC and the ones to skip#
G1 is the answer for effectively every Minecraft server. It is generational, it is concurrent for most of its work, and the flag set above is tuned for exactly this workload.
ZGC targets sub-millisecond pauses regardless of heap size. On Java 21 the generational version is opted into with -XX:+ZGenerational; on newer releases it is the default and the non-generational mode is on its way out, so check what your JDK expects before using it. It genuinely helps on very large heaps - a 24 GB modded server on a machine with cores to spare - and it costs more CPU and more off-heap memory than G1. Do not use it on a 4 GB plan with one and a half cores; you will pay for concurrency you cannot afford.
Shenandoah, available in Temurin and Corretto builds, is the other low-pause collector and behaves similarly. Both are worth trying only after you have proved with a GC log that pause time, not tick work, is what is hurting you.
Parallel GC (-XX:+UseParallelGC) is a throughput collector with long stop-the-world pauses. On a 1-2 GB server those pauses are short anyway, and it uses less CPU than G1, so it is a defensible choice on the very smallest plans. Above about 4 GB it is the wrong tool.
CMS is gone. It was removed in Java 14, and any guide recommending -XX:+UseConcMarkSweepGC was written before that and should be closed.
The flags that do nothing at all, and which still circulate: -XX:+UseFastAccessorMethods and -XX:+AggressiveOpts were removed from the JVM years ago, -Xincgc went with CMS, and -XX:+OptimizeStringConcat has been default behaviour for a decade. Setting -Xmn alongside G1 is worse than useless: it pins the young generation size and stops G1 adapting, which is the opposite of what the flags above are trying to achieve.
Diagnosing memory: logs, dumps and the four out-of-memory errors#
Turn on GC logging before you need it. It costs almost nothing and it is the only way to answer "was that stutter a pause?".
-Xlog:gc*:file=logs/gc.log:time,uptime,level,tags:filecount=5,filesize=10MWhat to look for in the result. Lines saying Pause Young (Normal) (G1 Evacuation Pause) at a few tens of milliseconds are the server working normally. Pause Young (Concurrent Start) is the marking cycle beginning, which should happen regularly and quietly. The two lines that mean something is wrong are To-space exhausted, which means G1 ran out of room to evacuate into and needs more reserve or more heap, and Pause Full (Allocation Failure), which means G1 gave up and did a single-threaded full collection. A full GC on a 6 GB heap is a multi-second freeze, and players will report it as a crash.
The out-of-memory errors are four different problems wearing the same name:
java.lang.OutOfMemoryError: Java heap space- the heap is genuinely full. Either raise-Xmxwithin the headroom you have, or find what is holding memory. A spark heap summary names the classes.java.lang.OutOfMemoryError: Metaspace- too many loaded classes. On a plugin server this is almost always caused by repeated use of/reload, which leaves old class loaders behind. Restart properly instead, and if you genuinely need more,-XX:MaxMetaspaceSize=512M.java.lang.OutOfMemoryError: unable to create new native thread- a process or memory limit, not a heap problem. Usually a plugin spawning threads without bound.- No error at all, and the log simply stops - the container hit its memory limit and was stopped from outside. Nothing in Java sees this coming, which is why the headroom table exists.
For the first case, -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=./dumps writes a dump you can analyse. Be aware that the dump is roughly the size of the heap, so a 10 GB server writes a 10 GB file to a disk that may not have room for it.
Once you know memory is not the problem, the next stop is the tick itself: why TPS drops and what to do, then reading a spark report to find the plugin or chunk responsible. What tick rate actually means explains why a server can sit at 20 TPS and still feel bad.
A worked example on a 6 GB plan#
Twenty players, Paper 1.21, fifteen plugins, a pre-generated world with a 5,000-block border.
$ java -Xms5G -Xmx5G -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 \ -Xlog:gc*:file=logs/gc.log:time,uptime:filecount=5,filesize=10M \ -jar paper.jar --nogui5G of a 6 GB plan, leaving a gigabyte for metaspace, code cache, Netty buffers and the JVM itself. Java 21 because the Minecraft version requires it. GC logging on because it is free. And that is the end of the JVM's contribution - if this server still stutters, the answer is in the Paper configuration and the plugin list, not in another flag. The Paper optimisation guide is where to go next, and reading a server load graph tells you whether you are looking at CPU, memory or disk.
On a panel-based host the flags go in the startup command or the JVM arguments field rather than a shell script. On RE:NODE that is the Startup tab, alongside the game and environment variables, and the console graphs memory against the plan limit so you can see immediately whether AlwaysPreTouch has flattened the line at the top.
FAQ#
Should I set -Xmx to my whole plan size?
No. The heap is only part of what the JVM uses, and the rest - metaspace, code cache, thread stacks, network buffers - lives outside it. Leave 500 MB on a small plan and 1-2 GB on a larger one, or the container gets stopped for exceeding its limit with no Java error to explain it.
Do Aikar's flags still work on Java 21?
Yes. Every flag in the set is still valid on current LTS releases, and the tuning is still appropriate for Minecraft's allocation pattern. The only thing that changed is that -XX:+UseG1GC is now stating a default rather than changing one.
Is more RAM going to fix my lag?
Only if memory is what is short. If the memory graph sits well below the limit while the server stutters, the bottleneck is the tick - a plugin, an entity count, or a view distance the CPU cannot carry. Adding heap in that situation makes pauses longer, not shorter.
Why does my server use exactly its maximum memory all the time?
-XX:+AlwaysPreTouch. It commits the whole heap at startup by design, so resident memory equals -Xmx from the first second. That is not a leak and it is not a problem, but it does mean the memory graph no longer shows you how much of the heap is live. Use a GC log or spark for that.
Can I run Minecraft 1.8 plugins on Java 21?
Usually not. Old plugins reach into JDK internals that later releases closed off, and the failures are obscure rather than clean. A legacy server should run the Java version that was current when it was built.
Does the garbage collector choice matter more than the flags?
For most servers, no. G1 with tuned flags is the right answer from about 2 GB to about 16 GB of heap, which covers nearly everyone. Switching collectors is something to try after a GC log has shown you that pause time specifically is the problem.




Комментарии
Полностью анонимно: без аккаунта, без почты, без cookie. Мы храним имя, которое вы ввели, текст и время - больше ничего. Количество ссылок ограничено, разметка не отображается.