Server Minecraft: risolvere "java.lang.OutOfMemoryError: Java heap space"
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
Killedoppure nel journalMain 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 condmesg | grep -i "out of memory", dove compare una riga comeOut 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 server | Giocatori | Xmx | RAM nel sistema |
|---|---|---|---|
| Vanilla o Paper, senza plugin | fino a 10 | 2G | 4 GB |
| Paper con 15-30 plugin | 10-30 | 4G | 8 GB |
| Paper, suite di plugin ampia, database | 30-80 | 6G-8G | 12-16 GB |
| Modpack leggero, fino a 120 mod | fino a 10 | 6G | 8 GB |
| Modpack medio, 150-250 mod | fino a 20 | 8G-10G | 16 GB |
| Modpack pesante, da 300 mod in su | fino a 20 | 10G-12G | 16-24 GB |
| Proxy (Velocity, BungeeCord) | a piacere | 512M-1G | 2 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-headlesseopenjdk-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 --memorymostra a colpo d'occhio occupazione dell'heap, comportamento del GC e aree fuori heap./spark heapsummarygenera una classifica delle classi per consumo di memoria, senza scrivere un dump grande diversi gigabyte./spark gcmostra 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:
jcmd PID GC.heap_info: l'heap occupato subito dopo una raccolta completa dovrebbe stare nettamente sotto il 70 percento di-Xmxe restare stabile nel tempo.ps -o rss= -C java: il valore dovrebbe assestarsi intorno aXmxpiù 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.free -h: available non dovrebbe mai scendere sotto i 500 MB circa.swapon --show: la quantità di swap occupata dovrebbe restare vicina a zero.- 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 chiamaG, nonGB. Sono ammessek,meg, in maiuscolo o in minuscolo.Initial heap size set to a larger value than the maximum heap sizesignifica 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 heapsignifica 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 conjava -version, lì deve comparire64-Bit Server VM.Unrecognized VM option 'UseG1GC'o simili indica una JVM troppo vecchia o sbagliata. Alcune flag sperimentali richiedono per forza un-XX:+UnlockExperimentalVMOptionsanteposto, 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?
Perché Xms deve essere uguale a Xmx?
Qual è la differenza fra un OutOfMemoryError e un processo che sparisce con Killed?
Come riconosco un memory leak causato da un plugin?
Più swap aiuta contro l'errore Java heap space?
Perché sul mio server non trovo jcmd e jmap?
2026 KernelHost GmbH. Tutti i diritti riservati. Questa guida è protetta dal diritto d'autore. La ripubblicazione su altri siti web, anche parziale o in forma modificata, non è consentita senza il nostro consenso scritto. Le citazioni con indicazione della fonte e un link sono le benvenute.

