Minecraft-server: "java.lang.OutOfMemoryError: Java heap space" oplossen

Gepubliceerd op 14 min leestijd

De melding java.lang.OutOfMemoryError: Java heap space betekent niet automatisch te weinig RAM. Zo dimensioneert u Xmx en Xms juist, spoort u geheugenlekken op en zet u swap verstandig in.

De server draait al drie uur, dan loopt hij vast, de tickrate zakt naar 2 en in de console staat:

[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(...)

De eerste reflex is bijna altijd dezelfde: meer RAM toewijzen. In ongeveer de helft van de gevallen is dat precies de verkeerde reactie, en in een deel daarvan wordt de zaak er meetbaar erger van. Dit artikel laat zien wat de melding werkelijk betekent, hoe u -Xmx en -Xms netjes dimensioneert, hoe u een geheugenlek van een echt geheugentekort onderscheidt en waaraan u ziet dat de oplossing standhoudt.

Wat de melding precies betekent, en wat niet

De Java-heap is het gebied waarin de JVM objecten opslaat: geladen chunks, entities, inventarissen en gegevens van plug-ins. De bovengrens daarvan legt u vast met -Xmx. Een OutOfMemoryError: Java heap space betekent: de JVM wilde een object aanmaken, de heap zat vol en de garbage collection kon niet genoeg ruimte vrijmaken. Over het vrije geheugen van het besturingssysteem zegt dat niets. Een server met 64 GB RAM geeft de melding net zo betrouwbaar wanneer -Xmx2G is ingesteld en de wereld 4 GB nodig heeft.

Daarvan moet u twee andere storingen goed onderscheiden, die er vaak mee verward worden:

  • Het proces verdwijnt zonder stacktrace, in het log staat alleen Killed of in het journal Main process exited, code=killed, status=9/KILL. Dat was de kernel, niet de JVM. De Linux-OOM-killer heeft toegeslagen omdat het volledige geheugen van het systeem op was. Controleren kunt u dat met dmesg | grep -i "out of memory", daar staat dan een regel als Out of memory: Killed process 1337 (java).
  • Java start helemaal niet en meldt Error occurred during initialization of VM / Could not reserve enough space for object heap. Dan is -Xmx groter dan wat het systeem kan leveren.

Dit onderscheid is de belangrijkste stap. Bij variant één is er te weinig heap, bij variant twee en drie te veel. Wie de gevallen verwisselt, draait aan de verkeerde knop.

Eerst meten, dan toewijzen

Voordat u ook maar één waarde wijzigt, hebt u twee getallen nodig: het werkelijk beschikbare geheugen en het huidige verbruik.

free -h
cat /proc/meminfo | grep -E 'MemTotal|MemAvailable|SwapTotal'

Doorslaggevend is MemAvailable, niet free. Linux gebruikt ongebruikt RAM als bestandscache, "free" is daarom bijna altijd klein en bijna altijd irrelevant.

Het werkelijke verbruik van de server ziet u zo:

ps -o pid,rss,cmd -C java

De RSS-waarde staat in kilobytes en is het geheugen dat het proces in RAM in beslag neemt. Die waarde is altijd groter dan -Xmx, en precies daar houden de meeste handleidingen op.

Waarom "zo veel mogelijk" gegarandeerd misgaat

Naast de heap heeft de JVM nog een hele reeks andere geheugengebieden nodig, die helemaal niet onder -Xmx vallen:

  • Metaspace: de geladen klassen. Bij een modpack met 300 mods is dat al snel 300 tot 600 MB.
  • Thread-stacks: elke thread krijgt ongeveer 1 MB. Chunk-workers, Netty-threads en de schedulers van plug-ins, dat telt op tot 100 tot 300 MB.
  • Direct buffers: Netty handelt het volledige netwerkverkeer af via geheugen buiten de heap. Bij veel gelijktijdige spelers zijn enkele honderden megabytes normaal.
  • Code-cache en GC-structuren: de JIT-compiler en de boekhouding van G1 zelf kosten grofweg 5 tot 10 procent van de heap.

Als betrouwbare vuistregel geldt: reken op Xmx plus 1 tot 1,5 GB voor de JVM, plus minstens 512 MB voor het besturingssysteem. Bij een modpack met veel mods eerder Xmx plus 2 GB.

Op een server met 8 GB RAM betekent dat -Xmx6G en niet -Xmx8G. Wie 8 toewijst, krijgt geen heapfout meer, maar iets ergers: een proces dat zonder waarschuwing door de kernel wordt afgeschoten, midden in het opslaan van de wereld. De heapfout is een nette, gedocumenteerde storing. De OOM-kill kan beschadigde regiobestanden achterlaten.

Er is nog een tweede reden tegen maximale toewijzing: een te grote heap maakt de garbage collection trager. G1 moet meer geheugen doorzoeken, de pauzes bij een mixed GC worden langer, en van een incidentele hapering wordt een merkbare freeze. Boven ongeveer 12 GB slaat die verhouding bij Minecraft doorgaans om naar het negatieve. Wie meer nodig heeft, kan de wereld beter opsplitsen dan de heap vergroten.

Vuistregels naar aantal spelers en modpack

Deze waarden zijn startpunten, geen natuurwetten. Ze gaan uit van een normaal grote wereld en actieve pregeneratie.

ServertypeSpelersXmxRAM in het systeem
Vanilla of Paper, zonder plug-instot 102G4 GB
Paper met 15 tot 30 plug-ins10 tot 304G8 GB
Paper, grote verzameling plug-ins, database30 tot 806G tot 8G12 tot 16 GB
Licht modpack, tot 120 modstot 106G8 GB
Middelzwaar modpack, 150 tot 250 modstot 208G tot 10G16 GB
Zwaar modpack, vanaf 300 modstot 2010G tot 12G16 tot 24 GB
Proxy (Velocity, BungeeCord)willekeurig512M tot 1G2 GB

Twee opmerkingen daarbij. Ten eerste schaalt de heapbehoefte bij modpacks vrijwel uitsluitend mee met het aantal mods en de grootte van de wereld, nauwelijks met het aantal spelers. Ten tweede is view-distance in de server.properties verreweg de effectiefste knop: van 10 naar 8 gaan bespaart vaak meer geheugen dan 2 GB extra heap, omdat het aantal geladen chunks kwadratisch met de kijkafstand groeit. simulation-distance op 6 zetten werkt daarnaast op de CPU-belasting.

Xms gelijk aan Xmx: het opwarmen

-Xms bepaalt met hoeveel heap de JVM start. Staat daar een kleinere waarde dan bij -Xmx, dan groeit de heap tijdens het draaien stukje bij beetje. Elke vergroting betekent een volledige garbage collection en verse page faults bij het besturingssysteem, en omdat G1 de heap ook weer laat krimpen, herhaalt dat spel zich telkens opnieuw. Precies daar komen de beruchte haperingen om de paar minuten vandaan, in de eerste uren na de start.

Stel -Xms altijd gelijk in aan -Xmx. Aangevuld met -XX:+AlwaysPreTouch raakt de JVM bij de start elke afzonderlijke geheugenpagina van de heap één keer aan. De start duurt daardoor, afhankelijk van de heapgrootte, 2 tot 15 seconden langer, maar tijdens het spelen treden er geen page faults meer op. Het prettige neveneffect: is -Xmx te groot gekozen, dan valt dat in de regel al bij de start op en niet pas drie uur later, midden in het spel.

Vertrouw daarbij echter niet op een korte proefrun met -version. Nagemeten: java -Xms4G -Xmx4G -XX:+AlwaysPreTouch -version liep in een tot 2 GB begrensde omgeving probleemloos door, omdat de aanroep eindigt voordat de heap daadwerkelijk in gebruik is. Een sluitend bewijs levert pas de echte serverstart, met daarna observatie van ps -o rss= -C java.

Een beproefd startcommando voor Paper ziet er daarmee zo uit (de zogenoemde 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

Vanaf 12 GB heap adviseert PaperMC aangepaste waarden: G1NewSizePercent=40, G1MaxNewSizePercent=50, G1HeapRegionSize=16M, G1ReservePercent=15 en InitiatingHeapOccupancyPercent=20.

De laatste twee regels leveren de eigenlijke winst op en ontbreken in bijna elke handleiding. -XX:+HeapDumpOnOutOfMemoryError schrijft bij een crash een volledige geheugendump weg, waarmee u de oorzaak later kunt aantonen. -XX:+ExitOnOutOfMemoryError beëindigt de JVM meteen, in plaats van hem in een halfdode toestand door te laten draaien waarin spelers verbinding maken en voortgang verliezen. Samen met Restart=on-failure in het unit-bestand ontstaat daaruit een nette herstart, zie daarvoor Minecraft-server automatisch starten en systemd-service aanmaken.

Houd hier rekening mee: de dumpmap moet bestaan en minstens zo veel ruimte hebben als -Xmx. Een heap van 8 GB levert een .hprof-bestand van 8 GB op. Is de schijf daarna vol, dan hebt u een tweede probleem, zie Schijf vol onder Linux.

Java-versie en systeemverschillen

Welke JVM u inzet, verandert het gedrag merkbaar:

  • Java 8 gebruikt standaard de parallel collector, niet G1. Hier duikt ook de variant java.lang.OutOfMemoryError: GC overhead limit exceeded op, die betekent dat meer dan 98 procent van de tijd in de garbage collection is doorgebracht. De GC-logging loopt via -XX:+PrintGCDetails -Xloggc:gc.log.
  • Vanaf Java 9 is G1 de standaard op alle machines met minstens twee cores en 1792 MB RAM. De logging loopt via de nieuwe unified logging: -Xlog:gc*:file=logs/gc.log:time,uptime:filecount=5,filesize=10M. De oude flags worden hier geweigerd, de server start dan niet.
  • Pakketsituatie: Debian 13 levert openjdk-21-jre-headless en openjdk-25-jre-headless, maar geen Java 17. Debian 12 levert Java 17, maar noch 21 noch 25. Ubuntu 22.04 en 24.04 hebben 8, 11, 17, 21 en 25. Wie een bepaalde versie nodig heeft die de distributie niet kent, neemt de Adoptium-repository (Temurin 8 tot 26 voor trixie, bookworm, noble en jammy). Details in Java 21 installeren op Debian en Java 17 installeren op Debian.

De valkuil waar bijna iedereen in trapt: de diagnosetools jcmd, jmap, jstat en jstack zitten niet in de jre-headless-pakketten. Ze zitten in openjdk-XX-jdk-headless. Wie de server met de JRE draait, staat als het erop aankomt zonder gereedschap:

apt install -y openjdk-21-jdk-headless
dnf install -y java-21-openjdk-devel

De eerste regel geldt voor Debian en Ubuntu, de tweede voor AlmaLinux, Rocky Linux en Oracle Linux. Daarna staan jcmd en jmap onder /usr/bin/. Controleer dat met command -v jcmd en niet met which jcmd: in de minimale installatie van de Red Hat-familie ontbreekt which, en onder EL 10 is het sowieso afgeschaft.

Geheugenlekken door plug-ins herkennen

Een geheugenlek ziet er anders uit dan simpelweg te weinig geheugen. Het verschil zit in het verloop: bij een te kleine heap stabiliseert het verbruik zich na de start op een hoog niveau en blijft daar. Bij een lek stijgt de basiswaarde na elke volledige garbage collection verder door. Precies die basiswaarde is het kengetal waar het om draait.

Die kunt u rechtstreeks opvragen. Bepaal eerst de proces-id, forceer daarna een volledige collectie en bekijk de heap:

pgrep -f paper.jar
jmap -histo:live PID | head -30
jcmd PID GC.heap_info

Belangrijke aanwijzing: jmap -histo:live zet intern een volledige collectie in gang en werkt ook wanneer, zoals hierboven, -XX:+DisableExplicitGC is ingesteld. Het voor de hand liggende commando jcmd PID GC.run doet in deze opzet niets, omdat het via System.gc() loopt en juist dat is uitgeschakeld. Dat kost ervaringsgewijs een half uur verwarring.

Noteer de waarde direct na de start, daarna na twee, zes en twaalf uur. Stijgt de waarde na de volledige collectie gestaag, zonder dat er meer spelers online zijn, dan is het een lek. De klassenlijst uit het histogram wijst meestal al de richting: massaal ItemStack, CraftPlayer van allang uitgelogde spelers of een object met de pakketnaam van een plug-in.

Veel comfortabeler gaat dat met de plug-in spark, die beschikbaar is voor Paper, Fabric en Forge:

  • /spark healthreport --memory toont in één oogopslag het heapgebruik, het GC-gedrag en de gebieden buiten de heap.
  • /spark heapsummary maakt een ranglijst van de klassen op geheugenverbruik, zonder daarbij een dump van meerdere gigabytes weg te schrijven.
  • /spark gc toont de frequentie en de duur van de collecties.

Valt de verdenking op een bepaalde plug-in, dan is het tegenbewijs simpel: plug-in verwijderen, de server 24 uur laten draaien, de basiswaarde opnieuw meten. Klassieke kandidaten zijn plug-ins die spelersgegevens in maps cachen en bij het uitloggen niet opruimen, plus alles wat met wereldbewerking te maken heeft en een onbeperkte undo-geschiedenis bijhoudt.

Swap: reddingsboei, geen geheugenuitbreiding

Swap en een Java-heap zijn natuurlijke vijanden. De garbage collection raakt regelmatig grote delen van de heap aan. Ligt daarvan ook maar een fractie op de schijf, dan wordt een pauze van 50 milliseconden er een van 20 seconden en geldt de server als vastgelopen. Reken swap nooit mee in de heapgrootte.

Toch hoort swap aanwezig te zijn, alleen dan klein en traag. Hij werkt als buffer, zodat kortstondige pieken niet meteen de OOM-killer in werking zetten, en hij vangt zelden gebruikte pagina's van andere services op. Aan te raden zijn 2 tot 4 GB en een lage neiging om te swappen:

swapon --show
cat /proc/sys/vm/swappiness
sysctl -w vm.swappiness=10

Permanent legt u dat vast in /etc/sysctl.d/99-swappiness.conf. Het inrichten in detail staat in Swap inrichten en out-of-memory voorkomen.

Een bijzonder geval verdient aandacht: -XX:+AlwaysPreTouch in combinatie met te weinig RAM. Omdat bij de start elke heappagina wordt aangeraakt, verhuist het overtollige deel meteen naar de swap. De server start dan wel, maar is vanaf de eerste tick onbruikbaar traag. Start een server na het instellen van PreTouch ineens extreem stroef, dan is -Xmx te groot en niet PreTouch de schuldige.

Als harde bovengrens kunt u daarnaast MemoryMax in de systemd-unit zetten, bijvoorbeeld MemoryMax=7G bij 8 GB RAM. Dan treft de kernel bij een ontsporing gericht het Minecraft-proces en niet de database of de SSH-toegang.

Waaraan u ziet dat het echt verholpen is

Een herstart zonder onmiddellijke crash is geen bewijs. Controleer in plaats daarvan deze vijf punten na 24 uur draaien onder normale belasting:

  1. jcmd PID GC.heap_info: de bezette heap direct na een volledige collectie hoort duidelijk onder 70 procent van -Xmx te liggen en na verloop van tijd stabiel te blijven.
  2. ps -o rss= -C java: de waarde hoort zich rond Xmx plus 1 tot 1,5 GB te stabiliseren en niet verder te klimmen. Stijgt die waarde terwijl de heap stabiel blijft, dan zit het lek buiten de heap, meestal in de metaspace of in direct buffers.
  3. free -h: available hoort nooit onder ongeveer 500 MB te zakken.
  4. swapon --show: de bezette hoeveelheid swap hoort dicht bij nul te blijven.
  5. Het GC-log: volledige collecties ("Pause Full") horen praktisch niet voor te komen, de gebruikelijke pauzes horen onder 200 milliseconden te blijven. Rijen full GC's kort na elkaar zijn het zekere voorteken van de volgende OutOfMemoryError, vaak een kwartier van tevoren.

Daarnaast laat /tps of /spark tps in het spel zien of de tickrate stabiel op 20,0 ligt. Een server die qua geheugen gezond is, houdt die waarde ook na uren vast.

Wanneer Java helemaal niet start

Vier meldingen in hun letterlijke bewoording, die bij het aanpassen van de geheugenwaarden vaak opduiken:

  • Invalid maximum heap size: -Xmx8GB is de meest voorkomende typefout. De eenheid heet G, niet GB. Toegestaan zijn k, m en g, in hoofdletters of in kleine letters.
  • Initial heap size set to a larger value than the maximum heap size betekent dat -Xms groter is dan -Xmx, meestal omdat bij het kopiëren maar één van beide waarden is aangepast.
  • Could not reserve enough space for object heap betekent dat de toewijzing het beschikbare geheugen overschrijdt. Op een 32-bits JVM houdt het bij krap 4 GB hoe dan ook op, ongeacht het aanwezige RAM. Controleren met java -version, daar moet 64-Bit Server VM staan.
  • Unrecognized VM option 'UseG1GC' of iets vergelijkbaars wijst op een te oude of verkeerde JVM. Sommige experimentele flags vereisen beslist een voorafgaand -XX:+UnlockExperimentalVMOptions, en wel vóór de betreffende flag op de commandoregel.

Waarmee de JVM uiteindelijk daadwerkelijk werkt, kunt u op elk moment natrekken. Zonder draaiende server via de standaardwaarden:

java -XX:+PrintFlagsFinal -version 2>/dev/null | grep -w MaxHeapSize

Twee kleinigheden daaraan zijn met opzet zo. De 2>/dev/null slikt de versiebanner in die de JVM naar de foutuitvoer schrijft en die anders ongefilterd langs de grep heen in de uitvoer belandt. En grep -w MaxHeapSize in plaats van grep -i maxheapsize levert echt maar één regel op: de onscherpe zoekopdracht vindt ook SoftMaxHeapSize, en wie maar één waarde verwacht, leest dan gemakkelijk de verkeerde af.

En bij het draaiende proces via de daadwerkelijk actieve flags:

jcmd PID VM.flags

Dat is de betrouwbaarste manier om de verrassend vaak voorkomende situatie boven tafel te krijgen dat een startscript wel is gewijzigd, maar de server nog met de oude waarden uit een tweede scriptbestand draait.


Samengevat: meet eerst of de heap eigenlijk wel het probleem is. Wijs daarna zo veel toe als de wereld nodig heeft en houd minstens 1,5 GB over voor de JVM en het systeem. Stel -Xms gelijk aan -Xmx, zet de heapdump aan voor noodgevallen en volg de basiswaarde na de volledige collectie over meerdere uren. Het verschil tussen "draait" en "draait stabiel" zit precies in die laatste stap.

Voor het basisinrichten van de server zelf vindt u de bijbehorende stappen onder Minecraft-server installeren op Debian en Checklist voor een nieuwe rootserver.

Veelgestelde vragen

Hoeveel RAM moet ik aan mijn Minecraft-server toewijzen?
Wijs zo veel toe als de wereld werkelijk nodig heeft en houd minstens 1 tot 1,5 GB over voor de JVM buiten de heap plus 512 MB voor het besturingssysteem. Op een server met 8 GB RAM betekent dat -Xmx6G. Vanilla met maximaal 10 spelers redt het met 2G, Paper met plug-ins en 30 spelers met 4G, een middelzwaar modpack heeft 8G tot 10G nodig. Boven ongeveer 12 GB heap worden de pauzes van de garbage collection langer in plaats van dat de prestaties toenemen.
Waarom moet Xms gelijk zijn aan Xmx?
Is -Xms kleiner dan -Xmx, dan groeit en krimpt de heap tijdens het draaien. Elke wijziging van de grootte zet een volledige garbage collection en nieuwe page faults in gang, wat merkbaar wordt als terugkerende haperingen. Gelijke waarden plus -XX:+AlwaysPreTouch reserveren de complete heap al bij de start. De start duurt daardoor enkele seconden langer, maar het draaien blijft daarna gelijkmatig.
Wat is het verschil tussen een OutOfMemoryError en een proces dat gewoon met Killed verdwijnt?
De OutOfMemoryError komt van de JVM en betekent: de met -Xmx vastgelegde heap zit vol. Een proces dat zonder stacktrace met Killed of status=9/KILL eindigt, is door de Linux-kernel beëindigd omdat het geheugen van het hele systeem op was. In het eerste geval is -Xmx te klein, in het tweede te groot. Het kernelgeval toont u aan met dmesg | grep -i "out of memory".
Hoe herken ik een geheugenlek door een plug-in?
Doorslaggevend is de bezette heap direct na een volledige garbage collection. Die forceert u met jmap -histo:live PID, aflezen doet u met jcmd PID GC.heap_info. Noteer de waarde na de start en daarna na twee, zes en twaalf uur. Blijft die stabiel, dan is de heap alleen te klein. Stijgt die gestaag bij een gelijk aantal spelers, dan is er sprake van een lek. De plug-in spark levert dezelfde analyse comfortabeler via /spark healthreport --memory en /spark heapsummary.
Helpt meer swap tegen de Java-heap-space-fout?
Nee. Swap vergroot de heap niet, want de bovengrens daarvan staat in -Xmx. Erger nog: een heap die in de swap belandt, maakt de garbage collection extreem traag, van een pauze van 50 milliseconden worden 20 seconden freeze. Zinvol zijn 2 tot 4 GB swap met vm.swappiness=10 als buffer tegen de OOM-killer, nooit als ingecalculeerde geheugenuitbreiding.
Waarom vind ik jcmd en jmap niet op mijn server?
Deze tools zitten niet in de pakketten openjdk-XX-jre-headless, maar alleen in openjdk-XX-jdk-headless. Wie de server met alleen het runtimepakket draait, moet het JDK-pakket alsnog installeren, bijvoorbeeld met apt install openjdk-21-jdk-headless. Let daarbij op de pakketsituatie: Debian 13 levert Java 21 en 25, Debian 12 levert Java 17.

Minecraft Java JVM Gameserver Probleemoplossing Werkgeheugen Garbage Collection Linux