Serwer Minecraft: jak naprawić błąd „java.lang.OutOfMemoryError: Java heap space”

Opublikowano 12 min czytania

Komunikat java.lang.OutOfMemoryError: Java heap space nie oznacza automatycznie za małej ilości pamięci RAM. Jak poprawnie dobrać Xmx i Xms, znaleźć wycieki pamięci i sensownie wykorzystać swap.

Serwer działa od trzech godzin, po czym się zawiesza, tickrate spada do 2, a w konsoli pojawia się:

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

Pierwszy odruch jest niemal zawsze ten sam: przydzielić więcej pamięci RAM. Mniej więcej w połowie przypadków to dokładnie zła reakcja, a w części z nich sprawa robi się mierzalnie gorsza. W tym artykule pokazujemy, co ten komunikat naprawdę oznacza, jak porządnie dobrać -Xmx oraz -Xms, jak odróżnisz wyciek pamięci od zwykłego jej braku i po czym poznasz, że poprawka rzeczywiście się utrzymała.

Co dokładnie oznacza ten komunikat, a czego nie

Heap Javy to obszar, w którym JVM odkłada obiekty: wczytane chunki, encje, ekwipunki, dane pluginów. Jego górną granicę ustawiasz przez -Xmx. Komunikat OutOfMemoryError: Java heap space znaczy tyle: JVM chciała utworzyć obiekt, heap był pełny, a garbage collection nie zdołała zwolnić wystarczająco dużo miejsca. O wolnej pamięci systemu operacyjnego nie mówi to zupełnie nic. Serwer z 64 GB RAM rzuci ten komunikat równie niezawodnie, jeśli ustawiono -Xmx2G, a świat potrzebuje 4 GB.

Trzeba to wyraźnie odróżnić od dwóch innych awarii, które bardzo często się z tym myli:

  • Proces znika bez stosu wywołań, w logu stoi tylko Killed, a w journalu Main process exited, code=killed, status=9/KILL. To było jądro, nie JVM. Zadziałał linuksowy OOM killer, bo skończyła się cała pamięć systemu. Sprawdzisz to poleceniem dmesg | grep -i "out of memory", pojawi się wtedy wiersz w rodzaju Out of memory: Killed process 1337 (java).
  • Java w ogóle się nie uruchamia i zgłasza Error occurred during initialization of VM / Could not reserve enough space for object heap. Wtedy -Xmx jest większe niż to, co system w ogóle jest w stanie dać.

To rozróżnienie jest najważniejszym krokiem. W pierwszym wariancie heapu jest za mało, w drugim i trzecim za dużo. Kto pomyli te przypadki, kręci nie tą śrubą, co trzeba.

Najpierw pomiar, potem przydział

Zanim zmienisz jakąkolwiek wartość, potrzebujesz dwóch liczb: faktycznie dostępnej pamięci oraz bieżącego zużycia.

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

Decyduje MemAvailable, a nie free. Linux wykorzystuje niewykorzystaną pamięć RAM jako cache plików, dlatego „free” jest prawie zawsze małe i prawie zawsze bez znaczenia.

Rzeczywiste zużycie serwera zobaczysz tak:

ps -o pid,rss,cmd -C java

Wartość RSS podawana jest w kilobajtach i oznacza pamięć, jaką proces zajmuje w RAM. Jest ona zawsze większa niż -Xmx, i dokładnie w tym miejscu większość poradników się kończy.

Dlaczego „jak najwięcej” na pewno się nie uda

Poza heapem JVM potrzebuje jeszcze całego szeregu innych obszarów pamięci, których -Xmx w ogóle nie obejmuje:

  • Metaspace: wczytane klasy. Przy modpacku z 300 modami to szybko od 300 do 600 MB.
  • Stosy wątków: każdy wątek dostaje około 1 MB. Workery chunków, wątki Netty, schedulery pluginów, to razem daje od 100 do 300 MB.
  • Direct buffers: Netty obsługuje cały ruch sieciowy przez pamięć spoza heapu. Przy wielu graczach jednocześnie kilkaset megabajtów to norma.
  • Code cache i struktury GC: kompilator JIT oraz własna księgowość G1 kosztują z grubsza od 5 do 10 procent heapu.

Sprawdzona reguła brzmi tak: licz Xmx plus od 1 do 1,5 GB dla JVM oraz co najmniej 512 MB dla systemu operacyjnego. Przy modpacku z dużą liczbą modów raczej Xmx plus 2 GB.

Na serwerze z 8 GB RAM oznacza to -Xmx6G, a nie -Xmx8G. Kto przydzieli 8, nie dostanie już błędu heapu, tylko coś gorszego: proces zestrzelony przez jądro bez ostrzeżenia, w samym środku zapisywania świata. Błąd heapu to czysta, udokumentowana awaria. Zabicie przez OOM potrafi zostawić po sobie uszkodzone pliki regionów.

Jest jeszcze drugi powód przeciwko maksymalnemu przydziałowi: zbyt duży heap spowalnia garbage collection. G1 musi przeszukać więcej pamięci, pauzy przy mixed GC się wydłużają, a z okazjonalnego przycięcia robi się wyraźne zawieszenie. Powyżej mniej więcej 12 GB bilans przy Minecrafcie z reguły przechyla się na minus. Kto potrzebuje więcej, powinien podzielić świat, zamiast powiększać heap.

Orientacyjne wartości według liczby graczy i modpacka

Te wartości to punkty wyjścia, a nie prawa natury. Zakładają świat normalnej wielkości oraz wykonane wcześniej pregenerowanie terenu.

Typ serweraGraczeXmxRAM w systemie
Vanilla lub Paper, bez pluginówdo 102G4 GB
Paper, od 15 do 30 pluginówod 10 do 304G8 GB
Paper, duży zestaw pluginów, baza danychod 30 do 806G do 8G12 do 16 GB
Lekki modpack, do 120 modówdo 106G8 GB
Średni modpack, od 150 do 250 modówdo 208G do 10G16 GB
Ciężki modpack, od 300 modówdo 2010G do 12G16 do 24 GB
Proxy (Velocity, BungeeCord)dowolna liczba512M do 1G2 GB

Dwie uwagi do tego. Po pierwsze, przy modpackach zapotrzebowanie na heap skaluje się niemal wyłącznie z liczbą modów i wielkością świata, prawie wcale z liczbą graczy. Po drugie, view-distance w pliku server.properties to najskuteczniejsza dźwignia w ogóle: zejście z 10 na 8 oszczędza często więcej pamięci niż 2 GB dodatkowego heapu, bo liczba wczytanych chunków rośnie z zasięgiem widzenia kwadratowo. Ustawienie simulation-distance na 6 działa dodatkowo na obciążenie CPU.

Xms równe Xmx: rozgrzewka

-Xms określa, z jak dużym heapem startuje JVM. Jeśli stoi tam wartość mniejsza niż przy -Xmx, heap rośnie w trakcie pracy kawałek po kawałku. Każde powiększenie oznacza pełne zbieranie garbage collection oraz świeże page faulty po stronie systemu operacyjnego, a ponieważ G1 pozwala heapowi także się kurczyć, cała zabawa powtarza się od nowa. Stąd właśnie biorą się osławione przycięcia co kilka minut w pierwszych godzinach po starcie.

Ustawiaj -Xms zawsze równe -Xmx. W połączeniu z -XX:+AlwaysPreTouch JVM przy starcie dotyka raz każdej pojedynczej strony pamięci heapu. Start trwa przez to od 2 do 15 sekund dłużej, zależnie od wielkości heapu, za to w czasie gry nie zdarzają się już żadne page faulty. Przyjemny efekt uboczny: jeśli -Xmx dobrano zbyt hojnie, z reguły widać to już przy starcie, a nie dopiero trzy godziny później w środku rozgrywki.

Nie polegaj przy tym na krótkim teście z -version. Sprawdzone w praktyce: java -Xms4G -Xmx4G -XX:+AlwaysPreTouch -version przeszło bez zarzutu w środowisku ograniczonym do 2 GB, bo wywołanie kończy się, zanim heap faktycznie zostanie użyty. Czystym dowodem jest dopiero prawdziwy start serwera i późniejsza obserwacja przez ps -o rss= -C java.

Sprawdzone polecenie startowe dla Papera wygląda więc tak (tak zwane flagi Aikara):

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

Od 12 GB heapu PaperMC zaleca zmienione wartości: G1NewSizePercent=40, G1MaxNewSizePercent=50, G1HeapRegionSize=16M, G1ReservePercent=15 oraz InitiatingHeapOccupancyPercent=20.

Dwa ostatnie wiersze to właściwy zysk i brakuje ich w niemal każdym poradniku. -XX:+HeapDumpOnOutOfMemoryError zapisuje przy awarii pełny obraz pamięci, na którym da się później udowodnić przyczynę. -XX:+ExitOnOutOfMemoryError kończy JVM natychmiast, zamiast zostawiać ją w półżywym stanie, w którym gracze się łączą i tracą postępy. Razem z Restart=on-failure w pliku unitu daje to czysty restart, zobacz Automatyczne uruchamianie serwera Minecraft oraz Tworzenie usługi systemd.

Weź to pod uwagę: katalog na zrzuty musi istnieć i mieć co najmniej tyle miejsca, ile wynosi -Xmx. Heap o wielkości 8 GB tworzy plik .hprof o wielkości 8 GB. Jeśli dysk się potem zapełni, masz drugi problem, zobacz Pełny dysk w systemie Linux.

Wersja Javy i różnice między systemami

To, której JVM używasz, wyraźnie zmienia zachowanie:

  • Java 8 używa domyślnie kolektora Parallel, nie G1. Tu pojawia się też wariant java.lang.OutOfMemoryError: GC overhead limit exceeded, który oznacza, że ponad 98 procent czasu poszło na garbage collection. Logowanie GC włącza się przez -XX:+PrintGCDetails -Xloggc:gc.log.
  • Od Javy 9 G1 jest standardem na wszystkich maszynach z co najmniej dwoma rdzeniami i 1792 MB RAM. Logowanie idzie przez nowe unified logging: -Xlog:gc*:file=logs/gc.log:time,uptime:filecount=5,filesize=10M. Stare flagi są tu odrzucane, serwer się nie uruchamia.
  • Sytuacja z pakietami: Debian 13 dostarcza openjdk-21-jre-headless oraz openjdk-25-jre-headless, ale żadnej Javy 17. Debian 12 dostarcza Javę 17, ale ani 21, ani 25. Ubuntu 22.04 i 24.04 mają 8, 11, 17, 21 oraz 25. Kto potrzebuje konkretnej wersji, której dystrybucja nie zna, sięga po repozytorium Adoptium (Temurin od 8 do 26 dla trixie, bookworm, noble i jammy). Szczegóły w Instalacja Javy 21 na Debianie oraz Instalacja Javy 17 na Debianie.

Pułapka, w którą wpada niemal każdy: narzędzia diagnostyczne jcmd, jmap, jstat oraz jstack w pakietach jre-headless nie występują. Siedzą w openjdk-XX-jdk-headless. Kto prowadzi serwer na samym JRE, w razie awarii zostaje bez narzędzi:

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

Pierwszy wiersz dotyczy Debiana i Ubuntu, drugi AlmaLinuksa, Rocky Linuksa oraz Oracle Linuksa. Potem jcmdjmap leżą w /usr/bin/. Sprawdź to poleceniem command -v jcmd, a nie which jcmd: w instalacji minimalnej rodziny Red Hat brakuje which, a w EL 10 jest on i tak wycofany.

Jak rozpoznać wycieki pamięci powodowane przez pluginy

Wyciek pamięci wygląda inaczej niż zwykły jej brak. Różnica tkwi w przebiegu: przy zbyt małym heapie zużycie po starcie ustala się na wysokim poziomie i tam zostaje. Przy wycieku wartość bazowa rośnie dalej po każdym pełnym zbieraniu garbage collection. To właśnie ta wartość bazowa jest wskaźnikiem, który się liczy.

Da się ją odczytać bezpośrednio. Najpierw ustal identyfikator procesu, potem wymuś pełne zbieranie i obejrzyj heap:

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

Ważna uwaga: jmap -histo:live wyzwala wewnętrznie pełne zbieranie i działa również wtedy, gdy ustawiono jak wyżej -XX:+DisableExplicitGC. Nasuwające się polecenie jcmd PID GC.run w tej konfiguracji nie robi nic, bo idzie przez System.gc(), a dokładnie to zostało wyłączone. Z doświadczenia kosztuje to pół godziny zamieszania.

Zapisz wartość zaraz po starcie, a potem po dwóch, sześciu i dwunastu godzinach. Jeśli wartość po pełnym zbieraniu stale rośnie, choć na serwerze nie ma więcej graczy, to wyciek. Lista klas z histogramu zwykle już pokazuje, w którą stronę to idzie: masowo ItemStack, CraftPlayer po dawno wylogowanych graczach albo obiekt z nazwą pakietu jakiegoś pluginu.

Znacznie wygodniej robi się to pluginem spark, dostępnym dla Papera, Fabrica oraz Forge:

  • /spark healthreport --memory pokazuje na jednym ekranie wykorzystanie heapu, zachowanie GC oraz obszary spoza heapu.
  • /spark heapsummary tworzy ranking klas według zużycia pamięci, nie zapisując przy tym zrzutu o wielkości kilku gigabajtów.
  • /spark gc pokazuje częstotliwość i czas trwania zbierania.

Jeśli podejrzenie pada na konkretny plugin, sprawdzenie jest proste: usuń plugin, zostaw serwer na 24 godziny, zmierz wartość bazową jeszcze raz. Klasyczni kandydaci to pluginy, które trzymają dane graczy w mapach i nie sprzątają ich przy wylogowaniu, oraz wszystko, co zajmuje się edycją świata i prowadzi nieograniczoną historię cofania.

Swap: koło ratunkowe, a nie powiększenie pamięci

Swap i heap Javy to naturalni wrogowie. Garbage collection regularnie dotyka dużych fragmentów heapu. Jeśli choćby ułamek z nich leży na dysku, z pauzy 50 milisekund robi się pauza 20 sekund i serwer uchodzi za zawieszony. Nigdy nie wliczaj swapa do wielkości heapu.

Mimo to swap powinien istnieć, tylko mały i leniwy. Działa jak bufor, żeby krótkie skoki nie uruchamiały od razu OOM killera, i przyjmuje rzadko używane strony innych usług. Zalecane są 2 do 4 GB oraz niska skłonność do wymiany:

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

Na stałe ustawia się to w pliku /etc/sysctl.d/99-swappiness.conf. Cała konfiguracja krok po kroku opisana jest w Konfiguracja swapa i zapobieganie brakom pamięci.

Jeden przypadek szczególny zasługuje na uwagę: -XX:+AlwaysPreTouch w połączeniu ze zbyt małą ilością RAM. Ponieważ przy starcie dotykana jest każda strona heapu, nadmiar od razu wędruje do swapa. Serwer wprawdzie wystartuje, ale od pierwszego ticku jest bezużytecznie wolny. Jeśli serwer po włączeniu PreTouch nagle startuje ekstremalnie ociężale, winne jest zbyt duże -Xmx, a nie PreTouch.

Jako twardą granicę można dodatkowo ustawić MemoryMax w unicie systemd, na przykład MemoryMax=7G przy 8 GB RAM. Wtedy przy wykolejeniu jądro trafia celowo w proces Minecrafta, a nie w bazę danych czy dostęp SSH.

Po czym poznasz, że problem naprawdę zniknął

Restart bez natychmiastowej awarii to żaden dowód. Sprawdź zamiast tego te pięć punktów po 24 godzinach pracy przy normalnym obciążeniu:

  1. jcmd PID GC.heap_info: zajęty heap zaraz po pełnym zbieraniu powinien leżeć wyraźnie poniżej 70 procent wartości -Xmx i pozostawać stabilny w czasie.
  2. ps -o rss= -C java: wartość powinna ustabilizować się mniej więcej na Xmx plus 1 do 1,5 GB i dalej nie rosnąć. Jeśli rośnie, a heap pozostaje stabilny, wyciek jest poza heapem, typowo w metaspace albo w direct buffers.
  3. free -h: available nie powinno nigdy spaść poniżej mniej więcej 500 MB.
  4. swapon --show: zajęta ilość swapa powinna pozostawać bliska zeru.
  5. Log GC: pełne cykle zbierania („Pause Full”) praktycznie nie powinny się zdarzać, a zwykłe pauzy powinny zostawać poniżej 200 milisekund. Pełne GC występujące seriami, jedno po drugim, to pewna zapowiedź kolejnego OutOfMemoryError, często z kwadransowym wyprzedzeniem.

Dodatkowo /tps lub /spark tps pokazuje w grze, czy tickrate stoi stabilnie na 20,0. Serwer zdrowy pamięciowo utrzymuje tę wartość również po wielu godzinach.

Kiedy Java w ogóle nie startuje

Cztery komunikaty w oryginalnym brzmieniu, które typowo pojawiają się przy zmienianiu wartości pamięci:

  • Invalid maximum heap size: -Xmx8GB to najczęstsza literówka. Jednostka nazywa się G, nie GB. Dozwolone są k, m oraz g, pisane wielką albo małą literą.
  • Initial heap size set to a larger value than the maximum heap size oznacza, że -Xms jest większe niż -Xmx, najczęściej dlatego, że przy kopiowaniu poprawiono tylko jedną z dwóch wartości.
  • Could not reserve enough space for object heap znaczy, że przydział przekracza dostępną pamięć. Na 32-bitowej JVM granica leży w każdym przypadku tuż poniżej 4 GB, niezależnie od zainstalowanej pamięci RAM. Sprawdzisz to poleceniem java -version, musi tam stać 64-Bit Server VM.
  • Unrecognized VM option 'UseG1GC' albo podobny komunikat wskazuje na zbyt starą lub niewłaściwą JVM. Część flag eksperymentalnych wymaga koniecznie poprzedzającego -XX:+UnlockExperimentalVMOptions, i to przed daną flagą w wierszu poleceń.

To, z czym JVM ostatecznie pracuje, da się w każdej chwili zweryfikować. Bez działającego serwera, po wartościach domyślnych:

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

Dwa drobiazgi są tu celowe. 2>/dev/null połyka baner wersji, który JVM wypisuje na wyjście błędów i który inaczej trafiłby do wyniku nieprzefiltrowany, obok grepa. A grep -w MaxHeapSize zamiast grep -i maxheapsize daje naprawdę tylko jeden wiersz: nieostre wyszukiwanie znajduje dodatkowo SoftMaxHeapSize, więc kto spodziewa się jednej wartości, łatwo odczyta tę niewłaściwą.

A na działającym procesie, po faktycznie aktywnych flagach:

jcmd PID VM.flags

To najpewniejszy sposób, żeby wykryć zaskakująco częstą sytuację, w której skrypt startowy wprawdzie zmieniono, ale serwer nadal działa ze starymi wartościami z drugiego pliku skryptu.


Podsumowując: najpierw zmierz, czy problemem w ogóle jest heap. Potem przydziel tyle, ile potrzebuje świat, i zostaw co najmniej 1,5 GB dla JVM oraz systemu. Ustaw -Xms równe -Xmx, włącz zrzut heapu na wypadek awarii i obserwuj wartość bazową po pełnym zbieraniu przez kilka godzin. Różnica między „działa” a „działa stabilnie” leży dokładnie w tym ostatnim kroku.

Podstawową konfigurację samego serwera znajdziesz w Instalacja serwera Minecraft na Debianie oraz Lista kontrolna dla nowego serwera root.

Najczęstsze pytania

Ile pamięci RAM przydzielić serwerowi Minecraft?
Przydziel tyle, ile świat faktycznie potrzebuje, i zostaw co najmniej 1 do 1,5 GB dla JVM poza heapem oraz 512 MB dla systemu operacyjnego. Na serwerze z 8 GB RAM oznacza to -Xmx6G. Vanilla z maksymalnie 10 graczami poradzi sobie z 2G, Paper z pluginami i 30 graczami z 4G, średni modpack potrzebuje od 8G do 10G. Powyżej mniej więcej 12 GB heapu pauzy garbage collection się wydłużają, zamiast żeby wydajność rosła.
Dlaczego Xms ma być równe Xmx?
Jeśli -Xms jest mniejsze niż -Xmx, heap w trakcie pracy rośnie i się kurczy. Każda zmiana rozmiaru wyzwala pełne zbieranie garbage collection oraz nowe page faulty, co odczuwa się jako powracające przycięcia. Równe wartości plus -XX:+AlwaysPreTouch rezerwują cały heap już przy starcie. Start trwa przez to kilka sekund dłużej, za to praca serwera jest równa.
Czym różni się OutOfMemoryError od procesu, który po prostu znika z komunikatem Killed?
OutOfMemoryError pochodzi od JVM i znaczy tyle: heap wyznaczony przez -Xmx jest pełny. Proces, który kończy się bez stosu wywołań komunikatem Killed albo status=9/KILL, zakończyło jądro Linuksa, bo pamięci zabrakło całemu systemowi. W pierwszym przypadku -Xmx jest za małe, w drugim za duże. Przypadek jądra udowodnisz poleceniem dmesg | grep -i "out of memory".
Jak rozpoznać wyciek pamięci powodowany przez plugin?
Miarodajny jest zajęty heap zaraz po pełnym zbieraniu garbage collection. Wymusisz je poleceniem jmap -histo:live PID, a odczytasz przez jcmd PID GC.heap_info. Zapisz wartość po starcie oraz po dwóch, sześciu i dwunastu godzinach. Jeśli pozostaje stabilna, heap jest po prostu za mały. Jeśli stale rośnie przy tej samej liczbie graczy, masz wyciek. Plugin spark dostarcza tę samą analizę wygodniej, przez /spark healthreport --memory oraz /spark heapsummary.
Czy więcej swapa pomoże na błąd Java heap space?
Nie. Swap nie powiększa heapu, bo jego górna granica stoi w -Xmx. Co gorsza: wypchnięty na dysk heap potwornie spowalnia garbage collection, z pauzy 50 milisekund robi się 20 sekund zawieszenia. Sensowne są 2 do 4 GB swapa z vm.swappiness=10 jako bufor przeciw OOM killerowi, nigdy jako zaplanowane powiększenie pamięci.
Dlaczego nie znajduję jcmd ani jmap na swoim serwerze?
Tych narzędzi nie ma w pakietach openjdk-XX-jre-headless, są tylko w openjdk-XX-jdk-headless. Kto prowadzi serwer na samym pakiecie środowiska uruchomieniowego, musi doinstalować pakiet JDK, na przykład poleceniem apt install openjdk-21-jdk-headless. Zwróć przy tym uwagę na sytuację z pakietami: Debian 13 dostarcza Javę 21 i 25, Debian 12 dostarcza Javę 17.

Minecraft Java JVM Serwery gier Rozwiązywanie problemów Pamięć RAM Garbage Collection Linux