Server Minecraft: risolvere "java.lang.OutOfMemoryError: Java heap space"

Pubblicato il 14 min di lettura

Il messaggio java.lang.OutOfMemoryError: Java heap space non significa automaticamente che manca RAM. Come dimensionare Xmx e Xms, trovare i memory leak e usare lo swap con criterio.

Il server gira da tre ore, poi si blocca, il tickrate scende a 2 e in console compare questo:

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

Il primo riflesso è quasi sempre lo stesso: assegnare più RAM. In circa la metà dei casi è esattamente la reazione sbagliata e in una parte di questi peggiora la situazione in modo misurabile. Questo articolo spiega che cosa significa davvero il messaggio, come dimensionare correttamente -Xmx e -Xms, come distinguere un memory leak da una reale carenza di memoria e da che cosa capisci che la correzione ha tenuto.

Che cosa significa esattamente il messaggio, e che cosa no

L'heap Java è l'area in cui la JVM deposita i suoi oggetti: chunk caricati, entità, inventari, dati dei plugin. Il suo limite superiore lo stabilisci con -Xmx. Un OutOfMemoryError: Java heap space significa: la JVM voleva creare un oggetto, l'heap era pieno e la garbage collection non è riuscita a liberare spazio a sufficienza. Sulla memoria libera del sistema operativo non dice nulla. Un server con 64 GB di RAM produce lo stesso messaggio con la stessa affidabilità se è impostato -Xmx2G e il mondo ha bisogno di 4 GB.

Da questo vanno distinti due altri guasti che vengono confusi volentieri:

  • Il processo sparisce senza stacktrace, nel log compare soltanto Killed oppure nel journal Main process exited, code=killed, status=9/KILL. Qui è stato il kernel, non la JVM. È intervenuto l'OOM killer di Linux perché la memoria complessiva del sistema era esaurita. Puoi verificarlo con dmesg | grep -i "out of memory", dove compare una riga come Out of memory: Killed process 1337 (java).
  • Java non parte affatto e segnala Error occurred during initialization of VM / Could not reserve enough space for object heap. In questo caso -Xmx è più grande di quanto il sistema possa davvero mettere a disposizione.

Questa distinzione è il passo più importante. Nel primo caso l'heap è troppo piccolo, nel secondo e nel terzo è troppo grande. Chi confonde i casi gira la vite sbagliata.

Prima misurare, poi assegnare

Prima di modificare qualsiasi valore ti servono due numeri: la memoria realmente disponibile e il consumo attuale.

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

Quello che conta è MemAvailable, non free. Linux usa la RAM inutilizzata come cache dei file, per questo "free" è quasi sempre un valore piccolo e quasi sempre irrilevante.

Il consumo reale del server lo vedi così:

ps -o pid,rss,cmd -C java

Il valore RSS è espresso in kilobyte ed è la memoria che il processo occupa nella RAM. È sempre maggiore di -Xmx, ed è il punto in cui la maggior parte delle guide si ferma.

Perché "il più possibile" va male di sicuro

Oltre all'heap la JVM ha bisogno di tutta una serie di altre aree di memoria che -Xmx non copre affatto:

  • Metaspace: le classi caricate. Con un modpack da 300 mod si arriva in fretta a un valore compreso fra 300 e 600 MB.
  • Stack dei thread: ogni thread riceve circa 1 MB. Chunk worker, thread di Netty, scheduler dei plugin: la somma arriva a 100 fino a 300 MB.
  • Direct buffer: Netty gestisce tutto il traffico di rete tramite memoria esterna all'heap. Con molti giocatori contemporanei diverse centinaia di megabyte sono normali.
  • Code cache e strutture del GC: il compilatore JIT e la contabilità interna di G1 costano all'incirca dal 5 al 10 percento dell'heap.

Come regola pratica affidabile vale questa: metti in conto Xmx più 1 fino a 1,5 GB per la JVM e almeno altri 512 MB per il sistema operativo. Con un modpack ricco di mod meglio Xmx più 2 GB.

Su un server con 8 GB di RAM significa -Xmx6G e non -Xmx8G. Chi ne assegna 8 non ottiene più un errore di heap, ma qualcosa di peggio: un processo che il kernel abbatte senza preavviso, magari proprio mentre il mondo viene salvato. L'errore di heap è un guasto pulito e documentato. L'OOM kill può lasciarti file di regione corrotti.

C'è un secondo motivo contro l'assegnazione massima: un heap troppo grande rende più lenta la garbage collection. G1 deve esaminare più memoria, le pause di un mixed GC si allungano e da uno scatto occasionale nasce un freeze percepibile. Sopra i 12 GB circa, su Minecraft il rapporto di solito diventa negativo. Chi ha bisogno di più dovrebbe dividere il mondo invece di ingrandire l'heap.

Regole pratiche per numero di giocatori e modpack

Questi valori sono punti di partenza, non leggi di natura. Presuppongono un mondo di dimensioni normali e il pregenerating attivo.

Tipo di serverGiocatoriXmxRAM nel sistema
Vanilla o Paper, senza pluginfino a 102G4 GB
Paper con 15-30 plugin10-304G8 GB
Paper, suite di plugin ampia, database30-806G-8G12-16 GB
Modpack leggero, fino a 120 modfino a 106G8 GB
Modpack medio, 150-250 modfino a 208G-10G16 GB
Modpack pesante, da 300 mod in sufino a 2010G-12G16-24 GB
Proxy (Velocity, BungeeCord)a piacere512M-1G2 GB

Due precisazioni. Primo: nei modpack il fabbisogno di heap scala quasi esclusivamente con il numero di mod e con la dimensione del mondo, quasi per nulla con il numero di giocatori. Secondo: view-distance in server.properties è la leva più efficace in assoluto. Scendere da 10 a 8 fa risparmiare spesso più memoria di 2 GB di heap in più, perché il numero di chunk caricati cresce in modo quadratico con la distanza di visualizzazione. Impostare simulation-distance su 6 agisce in aggiunta sul carico della CPU.

Xms uguale a Xmx: il riscaldamento

-Xms stabilisce con quanto heap parte la JVM. Se lì c'è un valore più piccolo rispetto a -Xmx, l'heap cresce a poco a poco durante il funzionamento. Ogni ingrandimento comporta una raccolta completa della garbage collection e nuovi page fault a carico del sistema operativo, e siccome G1 lascia anche rimpicciolire l'heap, il gioco si ripete. Da qui arrivano esattamente i famigerati scatti ogni pochi minuti nelle prime ore dopo l'avvio.

Imposta -Xms sempre uguale a -Xmx. Con l'aggiunta di -XX:+AlwaysPreTouch la JVM tocca una volta sola, all'avvio, ogni singola pagina di memoria dell'heap. L'avvio dura così da 2 a 15 secondi in più a seconda della dimensione dell'heap, in cambio durante il gioco non si verificano più page fault. Il gradevole effetto collaterale: se -Xmx è stato scelto troppo grande, di regola te ne accorgi già all'avvio e non tre ore dopo, in piena partita.

Non fidarti però di una prova veloce con -version. Misurato sul campo: java -Xms4G -Xmx4G -XX:+AlwaysPreTouch -version è passato senza problemi in un ambiente limitato a 2 GB, perché la chiamata termina prima che l'heap venga davvero usato. Una prova pulita è soltanto l'avvio reale del server con successiva osservazione di ps -o rss= -C java.

Un comando di avvio collaudato per Paper si presenta quindi così (le cosiddette flag di Aikar):

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

Dai 12 GB di heap in su PaperMC consiglia valori adattati: G1NewSizePercent=40, G1MaxNewSizePercent=50, G1HeapRegionSize=16M, G1ReservePercent=15 e InitiatingHeapOccupancyPercent=20.

Le ultime due righe sono il vero guadagno e mancano in quasi ogni guida. -XX:+HeapDumpOnOutOfMemoryError scrive al momento del crash un'immagine completa della memoria, con cui più tardi puoi dimostrare la causa. -XX:+ExitOnOutOfMemoryError chiude subito la JVM invece di lasciarla girare in uno stato mezzo morto, nel quale i giocatori si collegano e perdono progressi. Insieme a Restart=on-failure nel file di unit ne nasce un riavvio pulito, vedi a questo proposito Avviare automaticamente un server Minecraft e Creare un servizio systemd.

Mettilo in conto: la directory dei dump deve esistere e avere almeno tanto spazio quanto -Xmx. Un heap da 8 GB genera un file .hprof da 8 GB. Se poi il disco è pieno, hai un secondo problema, vedi Disco pieno su Linux.

Versione di Java e differenze di sistema

La JVM che utilizzi cambia il comportamento in modo percepibile:

  • Java 8 usa di default il collector Parallel, non G1. Qui compare anche la variante java.lang.OutOfMemoryError: GC overhead limit exceeded, che significa che oltre il 98 percento del tempo è stato speso nella garbage collection. Il logging del GC passa da -XX:+PrintGCDetails -Xloggc:gc.log.
  • Da Java 9 in poi G1 è lo standard su tutte le macchine con almeno due core e 1792 MB di RAM. Il logging passa dal nuovo unified logging: -Xlog:gc*:file=logs/gc.log:time,uptime:filecount=5,filesize=10M. Le vecchie flag qui vengono rifiutate e il server non parte.
  • Situazione dei pacchetti: Debian 13 fornisce openjdk-21-jre-headless e openjdk-25-jre-headless, ma nessun Java 17. Debian 12 fornisce Java 17, ma né 21 né 25. Ubuntu 22.04 e 24.04 hanno 8, 11, 17, 21 e 25. Chi ha bisogno di una versione precisa che la distribuzione non conosce ricorre al repository Adoptium (Temurin da 8 a 26 per trixie, bookworm, noble e jammy). Dettagli in Installare Java 21 su Debian e Installare Java 17 su Debian.

La trappola in cui cade quasi chiunque: gli strumenti di diagnosi jcmd, jmap, jstat e jstack non sono contenuti nei pacchetti jre-headless. Si trovano in openjdk-XX-jdk-headless. Chi fa girare il server con il solo JRE, nel momento critico si ritrova senza strumenti:

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

La prima riga vale per Debian e Ubuntu, la seconda per AlmaLinux, Rocky Linux e Oracle Linux. Dopo di che jcmd e jmap si trovano in /usr/bin/. Verificalo con command -v jcmd e non con which jcmd: nell'installazione minima della famiglia Red Hat manca which, e su EL 10 è comunque deprecato.

Riconoscere i memory leak dei plugin

Un memory leak si presenta in modo diverso da una semplice carenza di memoria. La differenza sta nell'andamento: con un heap troppo piccolo il consumo dopo l'avvio si assesta su un livello alto e lì resta. Con un leak il valore di base continua a salire dopo ogni garbage collection completa. È proprio questo valore di base la metrica che conta.

Lo puoi interrogare direttamente. Prima ricava l'ID del processo, poi forza una raccolta completa e guarda l'heap:

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

Nota importante: jmap -histo:live innesca internamente una raccolta completa e funziona anche quando, come sopra, è impostato -XX:+DisableExplicitGC. Il comando più ovvio, jcmd PID GC.run, in questa configurazione non fa nulla, perché passa da System.gc() e proprio quello è stato disattivato. Per esperienza costa una mezz'ora di confusione.

Annota il valore subito dopo l'avvio, poi dopo due, sei e dodici ore. Se il valore dopo la raccolta completa sale di continuo senza che ci siano più giocatori online, è un leak. L'elenco delle classi nell'istogramma di solito mostra già dove sta andando: ItemStack a valanga, CraftPlayer di giocatori usciti da un pezzo oppure un oggetto con il nome di package di un plugin.

Molto più comodo è farlo con il plugin spark, disponibile per Paper, Fabric e Forge:

  • /spark healthreport --memory mostra a colpo d'occhio occupazione dell'heap, comportamento del GC e aree fuori heap.
  • /spark heapsummary genera una classifica delle classi per consumo di memoria, senza scrivere un dump grande diversi gigabyte.
  • /spark gc mostra frequenza e durata delle raccolte.

Se il sospetto cade su un plugin preciso, la controprova è semplice: rimuovi il plugin, lascia girare il server per 24 ore, misura di nuovo il valore di base. Candidati classici sono i plugin che tengono in cache i dati dei giocatori in strutture Map e non fanno pulizia al logout, oltre a tutto ciò che ha a che fare con la modifica del mondo e mantiene una cronologia di annullamento illimitata.

Swap: ancora di salvezza, non ampliamento della memoria

Swap e heap Java sono nemici naturali. La garbage collection tocca regolarmente grandi porzioni dell'heap. Se anche solo una frazione di queste si trova sul disco, da una pausa di 50 millisecondi ne nasce una di 20 secondi e il server risulta congelato. Non conteggiare mai lo swap nella dimensione dell'heap.

Ciononostante lo swap dovrebbe esserci, solo piccolo e pigro. Funziona da cuscinetto, così i picchi brevi non fanno scattare subito l'OOM killer, e accoglie le pagine usate di rado da altri servizi. Consigliabili sono 2 fino a 4 GB e una bassa tendenza allo swapping:

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

In modo permanente lo imposti in /etc/sysctl.d/99-swappiness.conf. La configurazione nel dettaglio è descritta in Configurare lo swap ed evitare gli out of memory.

Un caso particolare merita attenzione: -XX:+AlwaysPreTouch in combinazione con troppa poca RAM. Siccome all'avvio ogni pagina dell'heap viene toccata, la parte in eccesso finisce subito nello swap. Il server parte, sì, ma è lento in modo inutilizzabile fin dal primo tick. Se un server, dopo l'attivazione di PreTouch, parte improvvisamente in modo lentissimo, la colpa è di -Xmx troppo grande, non di PreTouch.

Come limite superiore rigido puoi impostare in aggiunta MemoryMax nella unit di systemd, per esempio MemoryMax=7G con 8 GB di RAM. Così, in caso di deragliamento, il kernel colpisce in modo mirato il processo di Minecraft e non il database o l'accesso SSH.

Da che cosa capisci che il problema è davvero risolto

Un riavvio senza crash immediato non è una prova. Verifica invece questi cinque punti dopo 24 ore di funzionamento sotto carico normale:

  1. jcmd PID GC.heap_info: l'heap occupato subito dopo una raccolta completa dovrebbe stare nettamente sotto il 70 percento di -Xmx e restare stabile nel tempo.
  2. ps -o rss= -C java: il valore dovrebbe assestarsi intorno a Xmx più 1 fino a 1,5 GB e non salire oltre. Se sale mentre l'heap resta stabile, il leak è fuori dall'heap, tipicamente nel Metaspace o nei direct buffer.
  3. free -h: available non dovrebbe mai scendere sotto i 500 MB circa.
  4. swapon --show: la quantità di swap occupata dovrebbe restare vicina a zero.
  5. Il log del GC: le raccolte complete ("Pause Full") non dovrebbero praticamente comparire e le pause abituali dovrebbero restare sotto i 200 millisecondi. Una serie di Full GC ravvicinati è il segnale sicuro del prossimo OutOfMemoryError, spesso un quarto d'ora prima.

In aggiunta, /tps ovvero /spark tps mostra nel gioco se il tickrate resta stabile a 20,0. Un server sano dal punto di vista della memoria mantiene questo valore anche dopo ore.

Quando Java non parte proprio

Quattro messaggi alla lettera che compaiono tipicamente quando si adattano i valori di memoria:

  • Invalid maximum heap size: -Xmx8GB è l'errore di battitura più frequente. L'unità si chiama G, non GB. Sono ammesse k, m e g, in maiuscolo o in minuscolo.
  • Initial heap size set to a larger value than the maximum heap size significa che -Xms è più grande di -Xmx, di solito perché durante una copia è stato adattato uno solo dei due valori.
  • Could not reserve enough space for object heap significa che l'assegnazione supera la memoria disponibile. Su una JVM a 32 bit si finisce in ogni caso a poco meno di 4 GB, indipendentemente dalla RAM presente. Controlla con java -version, lì deve comparire 64-Bit Server VM.
  • Unrecognized VM option 'UseG1GC' o simili indica una JVM troppo vecchia o sbagliata. Alcune flag sperimentali richiedono per forza un -XX:+UnlockExperimentalVMOptions anteposto, e precisamente prima della flag interessata nella riga di comando.

Con che cosa la JVM lavori davvero alla fine lo puoi controllare in qualsiasi momento. Senza server in esecuzione, tramite i valori di default:

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

Due dettagli qui sono voluti. Il 2>/dev/null inghiotte il banner di versione che la JVM scrive sull'uscita di errore e che altrimenti, non filtrato, passerebbe accanto al grep finendo nell'output. E grep -w MaxHeapSize al posto di grep -i maxheapsize restituisce davvero una sola riga: la ricerca imprecisa trova in più SoftMaxHeapSize, e chi si aspetta un unico valore legge facilmente quello sbagliato.

E sul processo in esecuzione, tramite le flag realmente attive:

jcmd PID VM.flags

È la via più affidabile per scoprire la situazione sorprendentemente diffusa in cui uno script di avvio è stato sì modificato, ma il server gira ancora con i vecchi valori presi da un secondo file di script.


In sintesi: misura prima di tutto se il problema è davvero l'heap. Poi assegna quanto serve al mondo e lascia almeno 1,5 GB per JVM e sistema. Imposta -Xms uguale a -Xmx, attiva l'heap dump per il caso critico e osserva il valore di base dopo la raccolta completa per diverse ore. La differenza fra "gira" e "gira stabile" sta esattamente in quest'ultimo passaggio.

Per la configurazione di base del server trovi i passaggi adatti in Installare un server Minecraft su Debian e Checklist per un nuovo server root.

Domande frequenti

Quanta RAM devo assegnare al mio server Minecraft?
Assegna quanta ne serve davvero al mondo e lascia liberi almeno 1 fino a 1,5 GB per la JVM fuori dall'heap, più 512 MB per il sistema operativo. Su un server con 8 GB di RAM significa -Xmx6G. Vanilla con al massimo 10 giocatori se la cava con 2G, Paper con plugin e 30 giocatori con 4G, un modpack medio ha bisogno di 8G fino a 10G. Sopra i 12 GB di heap circa le pause della garbage collection si allungano invece di far salire le prestazioni.
Perché Xms deve essere uguale a Xmx?
Se -Xms è più piccolo di -Xmx, durante il funzionamento l'heap cresce e si rimpicciolisce. Ogni cambio di dimensione provoca una garbage collection completa e nuovi page fault, che si notano come scatti ricorrenti. Valori uguali più -XX:+AlwaysPreTouch riservano l'intero heap già all'avvio. L'avvio dura così qualche secondo in più, in cambio il funzionamento resta regolare.
Qual è la differenza fra un OutOfMemoryError e un processo che sparisce con Killed?
L'OutOfMemoryError arriva dalla JVM e significa: l'heap definito con -Xmx è pieno. Un processo che finisce senza stacktrace con Killed oppure status=9/KILL è stato terminato dal kernel Linux, perché la memoria dell'intero sistema era esaurita. Nel primo caso -Xmx è troppo piccolo, nel secondo troppo grande. Il caso kernel lo dimostri con dmesg | grep -i "out of memory".
Come riconosco un memory leak causato da un plugin?
Fa fede l'heap occupato subito dopo una garbage collection completa. La forzi con jmap -histo:live PID e la leggi con jcmd PID GC.heap_info. Annota il valore dopo l'avvio e poi dopo due, sei e dodici ore. Se resta stabile, l'heap è soltanto troppo piccolo. Se sale di continuo con lo stesso numero di giocatori, c'è un leak. Il plugin spark fornisce la stessa analisi in modo più comodo con /spark healthreport --memory e /spark heapsummary.
Più swap aiuta contro l'errore Java heap space?
No. Lo swap non ingrandisce l'heap, il cui limite superiore sta in -Xmx. Anzi, è peggio: un heap finito in swap rende lentissima la garbage collection, da una pausa di 50 millisecondi si passa a 20 secondi di freeze. Hanno senso 2 fino a 4 GB di swap con vm.swappiness=10 come cuscinetto contro l'OOM killer, mai come ampliamento di memoria messo in conto.
Perché sul mio server non trovo jcmd e jmap?
Questi strumenti non sono contenuti nei pacchetti openjdk-XX-jre-headless, ma soltanto in openjdk-XX-jdk-headless. Chi fa girare il server con il solo pacchetto di runtime deve installare in aggiunta il pacchetto JDK, per esempio con apt install openjdk-21-jdk-headless. Tieni presente la situazione dei pacchetti: Debian 13 fornisce Java 21 e 25, Debian 12 fornisce Java 17.

Minecraft Java JVM Game server Troubleshooting RAM Garbage collection Linux