Minecraft server: fixing "java.lang.OutOfMemoryError: Java heap space"

Published on 14 min read

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 shows Main 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 with dmesg | grep -i "out of memory", which then contains a line such as Out 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 -Xmx is 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 typePlayersXmxSystem RAM
Vanilla or Paper, no pluginsup to 102G4 GB
Paper with 15 to 30 plugins10 to 304G8 GB
Paper, large plugin suite, database30 to 806G to 8G12 to 16 GB
Light modpack, up to 120 modsup to 106G8 GB
Medium modpack, 150 to 250 modsup to 208G to 10G16 GB
Heavy modpack, 300 mods and upup to 2010G to 12G16 to 24 GB
Proxy (Velocity, BungeeCord)any512M to 1G2 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 exceeded shows 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-headless and openjdk-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 --memory shows heap usage, GC behavior and non-heap areas at a glance.
  • /spark heapsummary produces a ranking of classes by memory usage without writing a dump that is several gigabytes in size.
  • /spark gc shows 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:

  1. jcmd PID GC.heap_info: the used heap right after a full collection should sit well below 70 percent of -Xmx and stay stable over time.
  2. ps -o rss= -C java: the value should settle at roughly Xmx plus 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.
  3. free -h: available should never drop below roughly 500 MB.
  4. swapon --show: the amount of swap in use should stay close to zero.
  5. 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: -Xmx8GB is the most common typo. The unit is called G, not GB. Allowed are k, m and g, in upper or lower case.
  • Initial heap size set to a larger value than the maximum heap size means that -Xms is larger than -Xmx, usually because only one of the two values was adjusted while copying.
  • Could not reserve enough space for object heap means 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 with java -version, it has to say 64-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?
Assign as much as the world actually needs and leave at least 1 to 1.5 GB for the JVM outside the heap plus 512 MB for the operating system. On a server with 8 GB of RAM that means -Xmx6G. Vanilla with up to 10 players gets by with 2G, Paper with plugins and 30 players with 4G, a medium modpack needs 8G to 10G. Above roughly 12 GB of heap the garbage collection pauses get longer instead of performance going up.
Why should Xms be equal to Xmx?
If -Xms is smaller than -Xmx, the heap grows and shrinks while the server runs. Every resize triggers a full garbage collection and new page faults, which shows up as recurring stutters. Identical values plus -XX:+AlwaysPreTouch reserve the entire heap right at startup. The start takes a few seconds longer, but operation stays smooth.
What is the difference between an OutOfMemoryError and a process that simply disappears with Killed?
The OutOfMemoryError comes from the JVM and means that the heap defined by -Xmx is full. A process that ends without a stack trace with Killed or status=9/KILL was terminated by the Linux kernel because the memory of the entire system was exhausted. In the first case -Xmx is too small, in the second it is too large. You can prove the kernel case with dmesg | grep -i "out of memory".
How do I spot a memory leak caused by a plugin?
What counts is the used heap right after a full garbage collection. You can force one with jmap -histo:live PID and read it with jcmd PID GC.heap_info. Note the value after the start and again after two, six and twelve hours. If it stays stable, the heap is simply too small. If it keeps rising at the same player count, you have a leak. The spark plugin delivers the same analysis more conveniently via /spark healthreport --memory and /spark heapsummary.
Does more swap help against the Java heap space error?
No. Swap does not enlarge the heap, because its upper limit is defined by -Xmx. Worse still: a swapped-out heap makes garbage collection extremely slow, a 50 millisecond pause turns into a 20 second freeze. 2 to 4 GB of swap with vm.swappiness=10 make sense as a buffer against the OOM killer, never as planned extra memory.
Why can I not find jcmd and jmap on my server?
These tools are not contained in the openjdk-XX-jre-headless packages, only in openjdk-XX-jdk-headless. If you run the server on the pure runtime package, you have to install the JDK package afterwards, for example with apt install openjdk-21-jdk-headless. Keep the package availability in mind: Debian 13 ships Java 21 and 25, Debian 12 ships Java 17.

Minecraft Java JVM Gameserver Troubleshooting Arbeitsspeicher Garbage Collection Linux