Minecraft server: fixing "java.lang.OutOfMemoryError: Java heap space"
The message java.lang.OutOfMemoryError: Java heap space does not automatically mean too little RAM. How to size Xmx and Xms correctly, find memory leaks and use swap sensibly.
The server runs fine for three hours, then it freezes, the tick rate drops to 2 and the console shows:
[Server thread/ERROR]: Encountered an unexpected exception
java.lang.OutOfMemoryError: Java heap space
at java.base/java.util.Arrays.copyOf(Arrays.java:3537)
at it.unimi.dsi.fastutil.longs.Long2ObjectOpenHashMap.rehash(...)
The first reflex is almost always the same: assign more RAM. In roughly half of all cases that is exactly the wrong move, and in some of them it makes matters measurably worse. This article explains what the message really means, how to size -Xmx and -Xms properly, how to tell a memory leak apart from a genuine shortage of memory, and how you know that the fix has held.
What the message means, and what it does not
The Java heap is the area where the JVM stores objects: loaded chunks, entities, inventories, plugin data. You set its upper limit with -Xmx. An OutOfMemoryError: Java heap space means the JVM wanted to allocate an object, the heap was full, and garbage collection could not free up enough space. It says nothing about the free memory of the operating system. A server with 64 GB of RAM throws the message just as reliably when -Xmx2G is set and the world needs 4 GB.
Two other failure modes have to be kept apart from this one, and they get confused with it all the time:
- The process disappears without a stack trace, the log only says
Killed, or the journal showsMain process exited, code=killed, status=9/KILL. That was the kernel, not the JVM. The Linux OOM killer stepped in because the entire memory of the system was exhausted. You can check this withdmesg | grep -i "out of memory", which then contains a line such asOut of memory: Killed process 1337 (java). - Java does not start at all and reports
Error occurred during initialization of VM / Could not reserve enough space for object heap. In that case-Xmxis larger than what the system can provide in the first place.
Telling these apart is the most important step. In case one there is too little heap, in cases two and three there is too much. Mix them up and you end up turning the wrong screw.
Measure first, then allocate
Before you change any value, you need two numbers: the memory that is actually available and the current usage.
free -h
cat /proc/meminfo | grep -E 'MemTotal|MemAvailable|SwapTotal'
What matters is MemAvailable, not free. Linux uses unused RAM as a file cache, so "free" is almost always small and almost always irrelevant.
You see the real usage of the server like this:
ps -o pid,rss,cmd -C java
The RSS value is given in kilobytes and is the memory the process occupies in RAM. It is always larger than -Xmx, and this is the point where most guides stop.
Why "as much as possible" is guaranteed to backfire
Besides the heap, the JVM needs a whole range of further memory areas that -Xmx does not cover at all:
- Metaspace: the loaded classes. With a modpack of 300 mods this quickly reaches 300 to 600 MB.
- Thread stacks: every thread gets around 1 MB. Chunk workers, Netty threads, plugin schedulers, that adds up to 100 to 300 MB.
- Direct buffers: Netty handles all network traffic through memory outside the heap. With many concurrent players, several hundred megabytes are normal.
- Code cache and GC structures: the JIT compiler and G1's own bookkeeping cost roughly 5 to 10 percent of the heap.
A rule of thumb you can rely on: budget Xmx plus 1 to 1.5 GB for the JVM, plus at least 512 MB for the operating system. With a heavily modded pack, plan for Xmx plus 2 GB instead.
On a server with 8 GB of RAM that means -Xmx6G and not -Xmx8G. Assign 8 and you no longer get a heap error, you get something worse: a process that the kernel kills without warning, right in the middle of saving the world. The heap error is a clean, documented failure. An OOM kill can leave corrupted region files behind.
There is a second reason against maximum allocation: an oversized heap makes garbage collection slower. G1 has to scan more memory, the pauses of a mixed GC get longer, and an occasional stutter turns into a noticeable freeze. Above roughly 12 GB the trade-off usually turns negative for Minecraft. If you need more than that, split the world instead of growing the heap.
Rules of thumb by player count and modpack
These values are starting points, not laws of nature. They assume a world of normal size and terrain that has been pregenerated.
| Server type | Players | Xmx | System RAM |
|---|---|---|---|
| Vanilla or Paper, no plugins | up to 10 | 2G | 4 GB |
| Paper with 15 to 30 plugins | 10 to 30 | 4G | 8 GB |
| Paper, large plugin suite, database | 30 to 80 | 6G to 8G | 12 to 16 GB |
| Light modpack, up to 120 mods | up to 10 | 6G | 8 GB |
| Medium modpack, 150 to 250 mods | up to 20 | 8G to 10G | 16 GB |
| Heavy modpack, 300 mods and up | up to 20 | 10G to 12G | 16 to 24 GB |
| Proxy (Velocity, BungeeCord) | any | 512M to 1G | 2 GB |
Two notes on this. First, with modpacks the heap requirement scales almost entirely with the number of mods and the size of the world, hardly at all with the player count. Second, view-distance in server.properties is by far the most effective lever of all: going from 10 down to 8 often saves more memory than 2 GB of additional heap, because the number of loaded chunks grows quadratically with the view distance. Setting simulation-distance to 6 works on the CPU load as well.
Xms equal to Xmx: the warm-up
-Xms defines how much heap the JVM starts with. If the value there is smaller than -Xmx, the heap grows piece by piece while the server runs. Every increase means a full garbage collection and fresh page faults at the operating system level, and because G1 also lets the heap shrink again, the whole game repeats. This is exactly where the notorious stutters every few minutes in the first hours after a start come from.
Always set -Xms equal to -Xmx. Add -XX:+AlwaysPreTouch and the JVM touches every single memory page of the heap once at startup. Depending on the heap size, this makes the start 2 to 15 seconds longer, but in return no page faults happen while players are online. The pleasant side effect: if -Xmx is set too high, you normally notice it at startup and not three hours later in the middle of a session.
Do not rely on a quick test run with -version here, though. Measured: java -Xms4G -Xmx4G -XX:+AlwaysPreTouch -version completed without complaint in an environment capped at 2 GB, because the call ends before the heap is actually in use. The only proper proof is a real server start followed by watching ps -o rss= -C java.
A proven start command for Paper therefore looks like this (the so-called Aikar flags):
java -Xms6G -Xmx6G \
-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 \
-XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/opt/minecraft/dumps \
-XX:+ExitOnOutOfMemoryError \
-jar paper.jar nogui
From 12 GB of heap upwards, PaperMC recommends adjusted values: G1NewSizePercent=40, G1MaxNewSizePercent=50, G1HeapRegionSize=16M, G1ReservePercent=15 and InitiatingHeapOccupancyPercent=20.
The last two lines are the actual win and are missing from almost every guide. -XX:+HeapDumpOnOutOfMemoryError writes a complete memory image when the crash happens, which lets you prove the cause afterwards. -XX:+ExitOnOutOfMemoryError terminates the JVM immediately instead of leaving it running in a half-dead state in which players connect and lose progress. Together with Restart=on-failure in the unit file this adds up to a clean restart, see Start a Minecraft server automatically and Create a systemd service.
Plan for this: the dump directory has to exist and needs at least as much space as -Xmx. An 8 GB heap produces an 8 GB .hprof file. If the disk is full afterwards, you have a second problem, see Disk full on Linux.
Java version and system differences
Which JVM you run changes the behavior noticeably:
- Java 8 uses the parallel collector by default, not G1. This is also where the variant
java.lang.OutOfMemoryError: GC overhead limit exceededshows up, which means that more than 98 percent of the time was spent in garbage collection. GC logging goes through-XX:+PrintGCDetails -Xloggc:gc.log. - From Java 9 onwards, G1 is the default on every machine with at least two cores and 1792 MB of RAM. Logging goes through the new unified logging:
-Xlog:gc*:file=logs/gc.log:time,uptime:filecount=5,filesize=10M. The old flags are rejected here and the server will not start. - Package availability: Debian 13 ships
openjdk-21-jre-headlessandopenjdk-25-jre-headless, but no Java 17. Debian 12 ships Java 17, but neither 21 nor 25. Ubuntu 22.04 and 24.04 have 8, 11, 17, 21 and 25. If you need a particular version that your distribution does not know about, use the Adoptium repository (Temurin 8 to 26 for trixie, bookworm, noble and jammy). Details in Install Java 21 on Debian and Install Java 17 on Debian.
The trap that catches almost everyone: the diagnostic tools jcmd, jmap, jstat and jstack are not included in the jre-headless packages. They sit in openjdk-XX-jdk-headless. If you run the server on the JRE alone, you are left without tools exactly when it counts:
apt install -y openjdk-21-jdk-headless
dnf install -y java-21-openjdk-devel
The first line applies to Debian and Ubuntu, the second to AlmaLinux, Rocky Linux and Oracle Linux. Afterwards jcmd and jmap live under /usr/bin/. Check that with command -v jcmd and not with which jcmd: the minimal installation of the Red Hat family does not include which, and on EL 10 it is deprecated anyway.
Spotting memory leaks caused by plugins
A memory leak looks different from a plain shortage of memory. The difference lies in the trend: with a heap that is too small, usage settles at a high level after the start and stays there. With a leak, the baseline keeps climbing after every full garbage collection. That baseline is exactly the metric that matters.
You can query it directly. First find the process ID, then force a full collection and look at the heap:
pgrep -f paper.jar
jmap -histo:live PID | head -30
jcmd PID GC.heap_info
Important note: jmap -histo:live internally triggers a full collection and works even when -XX:+DisableExplicitGC is set as shown above. The obvious command jcmd PID GC.run does nothing in this constellation, because it goes through System.gc() and that is precisely what has been switched off. Experience says this costs half an hour of confusion.
Note the value right after the start, then after two, six and twelve hours. If the value after the full collection keeps rising without more players being online, it is a leak. The class list from the histogram usually points the way already: masses of ItemStack, CraftPlayer objects of players who logged out long ago, or an object carrying the package name of a plugin.
The spark plugin, which is available for Paper, Fabric and Forge, makes this far more convenient:
/spark healthreport --memoryshows heap usage, GC behavior and non-heap areas at a glance./spark heapsummaryproduces a ranking of classes by memory usage without writing a dump that is several gigabytes in size./spark gcshows the frequency and duration of the collections.
When suspicion falls on a particular plugin, the counter-proof is simple: remove the plugin, let the server run for 24 hours, measure the baseline again. Classic candidates are plugins that cache player data in maps and do not clean up on logout, plus anything to do with world editing that keeps an unlimited undo history.
Swap: a safety net, not extra memory
Swap and a Java heap are natural enemies. Garbage collection regularly touches large parts of the heap. If even a fraction of it sits on disk, a 50 millisecond pause turns into a 20 second one and the server counts as frozen. Never factor swap into the heap size.
Swap should still be present, just small and reluctant. It acts as a buffer so that short spikes do not immediately trigger the OOM killer, and it takes in rarely used pages of other services. 2 to 4 GB and a low swappiness are the recommendation:
swapon --show
cat /proc/sys/vm/swappiness
sysctl -w vm.swappiness=10
You make this permanent in /etc/sysctl.d/99-swappiness.conf. The setup in detail is described in Set up swap and avoid out-of-memory.
One special case deserves attention: -XX:+AlwaysPreTouch combined with too little RAM. Because every heap page is touched at startup, the excess part goes straight into swap. The server does start, but it is uselessly slow from the very first tick. If a server suddenly starts extremely sluggishly after PreTouch has been enabled, -Xmx is too large and PreTouch is not to blame.
As an additional hard limit you can set MemoryMax in the systemd unit, for example MemoryMax=7G on a machine with 8 GB of RAM. If things then get out of hand, the kernel hits the Minecraft process specifically and not the database or your SSH access.
How you know it is really fixed
A restart without an immediate crash is no proof. Instead, check these five points after 24 hours of operation under normal load:
jcmd PID GC.heap_info: the used heap right after a full collection should sit well below 70 percent of-Xmxand stay stable over time.ps -o rss= -C java: the value should settle at roughlyXmxplus 1 to 1.5 GB and stop climbing. If it rises while the heap stays stable, the leak is outside the heap, typically in the metaspace or in direct buffers.free -h: available should never drop below roughly 500 MB.swapon --show: the amount of swap in use should stay close to zero.- The GC log: full collections ("Pause Full") should practically never occur, and the usual pauses should stay under 200 milliseconds. A series of full GCs in quick succession is the reliable warning sign of the next
OutOfMemoryError, often a quarter of an hour in advance.
In addition, /tps or /spark tps in the game shows whether the tick rate sits stable at 20.0. A server that is healthy in memory terms holds that value even after hours.
When Java does not even start
Four messages, quoted verbatim, that typically show up while adjusting the memory values:
Invalid maximum heap size: -Xmx8GBis the most common typo. The unit is calledG, notGB. Allowed arek,mandg, in upper or lower case.Initial heap size set to a larger value than the maximum heap sizemeans that-Xmsis larger than-Xmx, usually because only one of the two values was adjusted while copying.Could not reserve enough space for object heapmeans that the allocation exceeds the available memory. On a 32-bit JVM the ceiling is just under 4 GB in any case, regardless of the installed RAM. Check withjava -version, it has to say64-Bit Server VM.Unrecognized VM option 'UseG1GC'or similar points to a JVM that is too old or simply the wrong one. Some experimental flags strictly require a preceding-XX:+UnlockExperimentalVMOptions, and it has to come before the flag in question on the command line.
What the JVM actually ends up working with can be verified at any time. Without a running server, through the default values:
java -XX:+PrintFlagsFinal -version 2>/dev/null | grep -w MaxHeapSize
Two small details in there are deliberate. The 2>/dev/null swallows the version banner that the JVM writes to standard error and that would otherwise slip past the grep unfiltered into the output. And grep -w MaxHeapSize instead of grep -i maxheapsize really returns only one line: the fuzzy search additionally matches SoftMaxHeapSize, and anyone expecting a single value easily reads the wrong one.
And on the running process, through the flags that are actually active:
jcmd PID VM.flags
This is the most reliable way to uncover the surprisingly common situation in which a start script was indeed changed, but the server still runs with the old values from a second script file.
In summary: measure first whether the heap is the problem at all. Then allocate as much as the world needs and leave at least 1.5 GB for the JVM and the system. Set -Xms equal to -Xmx, enable the heap dump for the emergency, and watch the baseline after the full collection over several hours. The difference between "it runs" and "it runs stably" lies exactly in that last step.
For the basic setup of the server itself you will find the matching steps in Install a Minecraft server on Debian and Checklist for a new root server.
Frequently asked questions
How much RAM should I assign to my Minecraft server?
Why should Xms be equal to Xmx?
What is the difference between an OutOfMemoryError and a process that simply disappears with Killed?
How do I spot a memory leak caused by a plugin?
Does more swap help against the Java heap space error?
Why can I not find jcmd and jmap on my server?
2026 KernelHost GmbH. All rights reserved. This guide is protected by copyright. Republishing it on other websites, in whole, in part or in edited form, is not permitted without our written consent. Quoting with a source credit and a link is expressly welcome.

