Serveur Minecraft : corriger « java.lang.OutOfMemoryError: Java heap space »

Publié le 15 min de lecture

Le message java.lang.OutOfMemoryError: Java heap space ne signifie pas automatiquement un manque de RAM. Comment dimensionner correctement Xmx et Xms, trouver les fuites mémoire et utiliser le swap à bon escient.

Le serveur tourne depuis trois heures, puis il se fige, le tickrate tombe à 2 et la console affiche :

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

Le premier réflexe est presque toujours le même : allouer plus de RAM. Dans près de la moitié des cas, c'est exactement la mauvaise réaction, et dans une partie de ces cas, cela aggrave même la situation de façon mesurable. Cet article montre ce que le message signifie réellement, comment dimensionner proprement -Xmx et -Xms, comment distinguer une fuite mémoire d'un véritable manque de mémoire, et à quoi vous reconnaissez que le correctif a tenu.

Ce que le message signifie exactement, et ce qu'il ne signifie pas

Le heap Java est la zone dans laquelle la JVM stocke ses objets : chunks chargés, entités, inventaires, données de plugins. Sa limite supérieure se définit avec -Xmx. Un OutOfMemoryError: Java heap space signifie ceci : la JVM a voulu créer un objet, le heap était plein et la garbage collection n'a pas réussi à libérer assez de place. Cela ne dit rien sur la mémoire libre du système d'exploitation. Un serveur doté de 64 GB de RAM produit le message avec la même fiabilité si -Xmx2G est défini alors que le monde a besoin de 4 GB.

Il faut justement distinguer ce cas de deux autres pannes que l'on confond volontiers :

  • Le processus disparaît sans stacktrace, le log ne contient que Killed, ou le journal Main process exited, code=killed, status=9/KILL. C'était le kernel, pas la JVM. L'OOM killer de Linux est intervenu parce que la mémoire entière du système était épuisée. Vous pouvez le vérifier avec dmesg | grep -i "out of memory" : une ligne du type Out of memory: Killed process 1337 (java) y apparaît alors.
  • Java ne démarre pas du tout et signale Error occurred during initialization of VM / Could not reserve enough space for object heap. Dans ce cas, -Xmx dépasse ce que le système peut réellement fournir.

Cette distinction est l'étape la plus importante. Dans le premier cas, le heap est trop petit ; dans les cas deux et trois, il est trop grand. Confondre les situations revient à tourner le mauvais bouton.

Mesurer d'abord, allouer ensuite

Avant de modifier la moindre valeur, il vous faut deux chiffres : la mémoire réellement disponible et la consommation actuelle.

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

Ce qui compte, c'est MemAvailable, pas free. Linux utilise la RAM inoccupée comme cache de fichiers : « free » est donc presque toujours faible et presque toujours sans intérêt.

Voici comment voir la consommation réelle du serveur :

ps -o pid,rss,cmd -C java

La valeur RSS est exprimée en kilo-octets et correspond à la mémoire que le processus occupe en RAM. Elle est toujours supérieure à -Xmx, et c'est précisément là que la plupart des tutoriels s'arrêtent.

Pourquoi « le plus possible » échoue à coup sûr

En plus du heap, la JVM a besoin de toute une série d'autres zones mémoire que -Xmx ne couvre absolument pas :

  • Metaspace : les classes chargées. Avec un modpack de 300 mods, cela représente vite 300 à 600 MB.
  • Thread stacks : chaque thread reçoit environ 1 MB. Workers de chunks, threads Netty, schedulers de plugins, le total monte à 100 ou 300 MB.
  • Direct buffers : Netty fait transiter la totalité du trafic réseau par de la mémoire située hors du heap. Avec beaucoup de joueurs simultanés, plusieurs centaines de mégaoctets sont normaux.
  • Code cache et structures du GC : le compilateur JIT et la comptabilité interne de G1 coûtent grossièrement 5 à 10 % du heap.

Règle empirique fiable : comptez Xmx plus 1 à 1,5 GB pour la JVM, plus au moins 512 MB pour le système d'exploitation. Avec un modpack chargé en mods, prévoyez plutôt Xmx plus 2 GB.

Sur un serveur de 8 GB de RAM, cela donne -Xmx6G et non -Xmx8G. En allouant 8, vous n'obtenez plus une erreur de heap, mais quelque chose de pire : un processus abattu sans préavis par le kernel, en pleine sauvegarde du monde. L'erreur de heap est une panne propre et documentée. L'OOM kill, lui, peut laisser derrière lui des fichiers de région corrompus.

Il existe une deuxième raison de ne pas allouer le maximum : un heap trop grand ralentit la garbage collection. G1 doit parcourir davantage de mémoire, les pauses d'un mixed GC s'allongent, et un micro-freeze occasionnel devient un gel bien perceptible. Au-delà d'environ 12 GB, le rapport bascule en général dans le négatif sur Minecraft. Si vous avez besoin de plus, mieux vaut découper le monde qu'agrandir le heap.

Règles empiriques selon le nombre de joueurs et le modpack

Ces valeurs sont des points de départ, pas des lois de la nature. Elles supposent un monde de taille normale et un pregenerating actif.

Type de serveurJoueursXmxRAM du système
Vanilla ou Paper, sans pluginsjusqu'à 102G4 GB
Paper avec 15 à 30 plugins10 à 304G8 GB
Paper, grosse suite de plugins, base de données30 à 806G à 8G12 à 16 GB
Modpack léger, jusqu'à 120 modsjusqu'à 106G8 GB
Modpack moyen, 150 à 250 modsjusqu'à 208G à 10G16 GB
Modpack lourd, à partir de 300 modsjusqu'à 2010G à 12G16 à 24 GB
Proxy (Velocity, BungeeCord)indifférent512M à 1G2 GB

Deux remarques à ce sujet. Premièrement, avec les modpacks, le besoin en heap dépend presque uniquement du nombre de mods et de la taille du monde, très peu du nombre de joueurs. Deuxièmement, view-distance dans server.properties est le levier le plus efficace de tous : passer de 10 à 8 économise souvent plus de mémoire que 2 GB de heap supplémentaires, car le nombre de chunks chargés croît de façon quadratique avec la distance d'affichage. Régler simulation-distance sur 6 agit en plus sur la charge CPU.

Xms égal à Xmx : la mise en chauffe

-Xms détermine la taille de heap avec laquelle la JVM démarre. Si cette valeur est inférieure à celle de -Xmx, le heap grandit petit à petit pendant le fonctionnement. Chaque agrandissement déclenche une collecte complète de la garbage collection et de nouveaux page faults côté système d'exploitation, et comme G1 laisse aussi le heap se rétracter, le manège recommence. C'est exactement de là que viennent les fameux micro-freezes toutes les quelques minutes durant les premières heures après le démarrage.

Définissez toujours -Xms à la même valeur que -Xmx. Complétée par -XX:+AlwaysPreTouch, la JVM touche une fois chaque page mémoire du heap au démarrage. Le démarrage prend ainsi 2 à 15 secondes de plus selon la taille du heap, mais plus aucun page fault ne survient en cours de partie. L'effet secondaire agréable : si -Xmx est trop élevé, cela se remarque en général dès le démarrage et pas trois heures plus tard, en pleine partie.

Ne vous fiez pas pour autant à un court essai avec -version. Vérification faite : java -Xms4G -Xmx4G -XX:+AlwaysPreTouch -version s'est exécuté sans broncher dans un environnement limité à 2 GB, car l'appel se termine avant que le heap ne soit réellement utilisé. La seule preuve valable reste un vrai démarrage du serveur, suivi d'une observation de ps -o rss= -C java.

Une commande de démarrage éprouvée pour Paper ressemble donc à ceci (les fameux flags 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

À partir de 12 GB de heap, PaperMC recommande des valeurs adaptées : G1NewSizePercent=40, G1MaxNewSizePercent=50, G1HeapRegionSize=16M, G1ReservePercent=15 et InitiatingHeapOccupancyPercent=20.

Les deux dernières lignes sont le véritable gain, et elles manquent dans presque tous les tutoriels. -XX:+HeapDumpOnOutOfMemoryError écrit une image mémoire complète au moment du crash, ce qui permet de prouver la cause plus tard. -XX:+ExitOnOutOfMemoryError arrête immédiatement la JVM au lieu de la laisser tourner dans un état à moitié mort, dans lequel des joueurs se connectent et perdent leur progression. Combiné à Restart=on-failure dans le fichier d'unité, cela donne un redémarrage propre, voir à ce sujet Démarrer automatiquement un serveur Minecraft et Créer un service systemd.

À prévoir : le répertoire de dump doit exister et disposer d'au moins autant d'espace que -Xmx. Un heap de 8 GB produit un fichier .hprof de 8 GB. Si le stockage est plein ensuite, vous avez un deuxième problème, voir Disque plein sous Linux.

Version de Java et différences entre systèmes

La JVM que vous utilisez modifie nettement le comportement :

  • Java 8 utilise par défaut le collecteur Parallel, pas G1. C'est là qu'apparaît également la variante java.lang.OutOfMemoryError: GC overhead limit exceeded, qui signifie que plus de 98 % du temps a été passé dans la garbage collection. La journalisation du GC passe par -XX:+PrintGCDetails -Xloggc:gc.log.
  • À partir de Java 9, G1 est le collecteur par défaut sur toutes les machines disposant d'au moins deux cœurs et de 1792 MB de RAM. La journalisation passe par le nouveau Unified Logging : -Xlog:gc*:file=logs/gc.log:time,uptime:filecount=5,filesize=10M. Les anciens flags sont ici rejetés et le serveur ne démarre pas.
  • Disponibilité des paquets : Debian 13 fournit openjdk-21-jre-headless et openjdk-25-jre-headless, mais pas Java 17. Debian 12 fournit Java 17, mais ni 21 ni 25. Ubuntu 22.04 et 24.04 proposent 8, 11, 17, 21 et 25. Si vous avez besoin d'une version précise que la distribution ne connaît pas, prenez le dépôt Adoptium (Temurin 8 à 26 pour trixie, bookworm, noble et jammy). Détails dans Installer Java 21 sur Debian et Installer Java 17 sur Debian.

Le piège qui touche presque tout le monde : les outils de diagnostic jcmd, jmap, jstat et jstack ne sont pas inclus dans les paquets jre-headless. Ils se trouvent dans openjdk-XX-jdk-headless. Si vous exploitez le serveur avec le seul JRE, vous vous retrouvez sans outil le jour où cela compte :

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

La première ligne vaut pour Debian et Ubuntu, la seconde pour AlmaLinux, Rocky Linux et Oracle Linux. Ensuite, jcmd et jmap se trouvent dans /usr/bin/. Vérifiez-le avec command -v jcmd et non avec which jcmd : dans l'installation minimale de la famille Red Hat, which est absent, et sous EL 10 il est de toute façon déprécié.

Détecter les fuites mémoire causées par les plugins

Une fuite mémoire ne ressemble pas à un simple manque de mémoire. La différence tient à l'évolution dans le temps : avec un heap trop petit, la consommation se stabilise à un niveau élevé après le démarrage et y reste. Avec une fuite, la valeur de base continue de monter après chaque garbage collection complète. C'est précisément cette valeur de base qui constitue l'indicateur déterminant.

Elle se relève directement. D'abord déterminer l'ID du processus, puis forcer une collecte complète et examiner le heap :

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

Remarque importante : jmap -histo:live déclenche en interne une collecte complète et fonctionne même lorsque -XX:+DisableExplicitGC est défini, comme plus haut. La commande évidente jcmd PID GC.run ne fait rien dans cette configuration, car elle passe par System.gc(), justement désactivé. L'expérience montre que cela coûte une bonne demi-heure de confusion.

Notez la valeur juste après le démarrage, puis après deux, six et douze heures. Si la valeur relevée après la collecte complète monte régulièrement sans qu'il y ait davantage de joueurs en ligne, il s'agit d'une fuite. La liste de classes issue de l'histogramme indique le plus souvent déjà la direction : des ItemStack en masse, des CraftPlayer de joueurs déconnectés depuis longtemps, ou un objet portant le nom de package d'un plugin.

C'est nettement plus confortable avec le plugin spark, disponible pour Paper, Fabric et Forge :

  • /spark healthreport --memory affiche d'un coup d'œil l'occupation du heap, le comportement du GC et les zones hors heap.
  • /spark heapsummary produit un classement des classes par consommation mémoire, sans écrire de dump de plusieurs gigaoctets.
  • /spark gc affiche la fréquence et la durée des collectes.

Lorsque les soupçons se portent sur un plugin précis, la contre-épreuve est simple : retirer le plugin, laisser tourner le serveur 24 heures, mesurer à nouveau la valeur de base. Les candidats classiques sont les plugins qui mettent en cache des données de joueurs dans des maps sans les nettoyer à la déconnexion, ainsi que tout ce qui touche à l'édition du monde et conserve un historique d'annulation illimité.

Swap : une bouée de secours, pas une extension de mémoire

Le swap et un heap Java sont des ennemis naturels. La garbage collection parcourt régulièrement de grandes parties du heap. Si ne serait-ce qu'une fraction se trouve sur le disque, une pause de 50 millisecondes devient une pause de 20 secondes et le serveur passe pour gelé. N'intégrez jamais le swap dans le calcul de la taille du heap.

Le swap doit malgré tout être présent, mais petit et peu sollicité. Il sert de tampon pour que des pics brefs ne déclenchent pas immédiatement l'OOM killer, et il accueille les pages rarement utilisées des autres services. Comptez 2 à 4 GB et une faible propension à la pagination :

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

La valeur se fixe durablement dans /etc/sysctl.d/99-swappiness.conf. La mise en place détaillée figure dans Configurer le swap et éviter les Out-of-Memory.

Un cas particulier mérite attention : -XX:+AlwaysPreTouch combiné à une RAM insuffisante. Comme chaque page du heap est touchée au démarrage, la partie excédentaire part immédiatement dans le swap. Le serveur démarre certes, mais il est inutilisablement lent dès le premier tick. Si un serveur devient soudain extrêmement poussif au démarrage après l'activation de PreTouch, c'est -Xmx qui est trop grand, PreTouch n'y est pour rien.

Comme limite dure supplémentaire, vous pouvez définir MemoryMax dans l'unité systemd, par exemple MemoryMax=7G avec 8 GB de RAM. En cas de dérapage, le kernel frappe alors précisément le processus Minecraft et non la base de données ou l'accès SSH.

Comment savoir que le problème est vraiment réglé

Un redémarrage sans crash immédiat ne prouve rien. Vérifiez plutôt ces cinq points après 24 heures d'exploitation sous charge normale :

  1. jcmd PID GC.heap_info : le heap occupé juste après une collecte complète doit rester nettement sous 70 % de -Xmx et rester stable dans la durée.
  2. ps -o rss= -C java : la valeur doit se stabiliser autour de Xmx plus 1 à 1,5 GB et ne plus grimper. Si elle monte alors que le heap reste stable, la fuite se situe hors du heap, typiquement dans le metaspace ou dans les direct buffers.
  3. free -h : available ne doit jamais descendre sous 500 MB environ.
  4. swapon --show : la quantité de swap occupée doit rester proche de zéro.
  5. Le log du GC : les collectes complètes (« Pause Full ») ne doivent pratiquement pas apparaître, et les pauses habituelles doivent rester sous 200 millisecondes. Des Full GC en série, rapprochées les unes des autres, annoncent à coup sûr le prochain OutOfMemoryError, souvent un quart d'heure à l'avance.

En complément, /tps ou /spark tps montre en jeu si le tickrate reste stable à 20,0. Un serveur sain du point de vue de la mémoire tient cette valeur même après des heures.

Quand Java refuse tout simplement de démarrer

Quatre messages, dans leur formulation exacte, qui apparaissent typiquement lors de l'ajustement des valeurs mémoire :

  • Invalid maximum heap size: -Xmx8GB est la faute de frappe la plus fréquente. L'unité s'écrit G, pas GB. Sont autorisés k, m et g, en majuscule comme en minuscule.
  • Initial heap size set to a larger value than the maximum heap size signifie que -Xms est plus grand que -Xmx, le plus souvent parce qu'une seule des deux valeurs a été ajustée lors d'un copier-coller.
  • Could not reserve enough space for object heap signifie que l'allocation dépasse la mémoire disponible. Sur une JVM 32 bits, la limite tombe de toute façon un peu en dessous de 4 GB, quelle que soit la RAM installée. Vérifiez avec java -version, la mention 64-Bit Server VM doit y figurer.
  • Unrecognized VM option 'UseG1GC' ou similaire indique une JVM trop ancienne ou inadaptée. Certains flags expérimentaux exigent impérativement un -XX:+UnlockExperimentalVMOptions placé avant le flag concerné dans la ligne de commande.

Les valeurs avec lesquelles la JVM travaille finalement peuvent être contrôlées à tout moment. Sans serveur en cours d'exécution, via les valeurs par défaut :

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

Deux détails y sont volontaires. Le 2>/dev/null avale la bannière de version que la JVM écrit sur la sortie d'erreur et qui, sinon, atterrit dans la sortie sans passer par le filtre de grep. Et grep -w MaxHeapSize plutôt que grep -i maxheapsize ne renvoie vraiment qu'une seule ligne : la recherche approximative trouve en plus SoftMaxHeapSize, et qui n'attend qu'une valeur lit vite la mauvaise.

Et sur le processus en cours d'exécution, via les flags réellement actifs :

jcmd PID VM.flags

C'est le moyen le plus fiable de mettre au jour une situation étonnamment répandue : un script de démarrage a bien été modifié, mais le serveur tourne encore avec les anciennes valeurs issues d'un deuxième fichier de script.


En résumé : mesurez d'abord si le heap est bien le problème. Allouez ensuite ce dont le monde a besoin et laissez au moins 1,5 GB à la JVM et au système. Définissez -Xms égal à -Xmx, activez le heap dump pour le jour où cela tourne mal et observez la valeur de base après la collecte complète pendant plusieurs heures. La différence entre « ça tourne » et « ça tourne de façon stable » se joue exactement dans cette dernière étape.

Pour la configuration de base du serveur lui-même, vous trouverez les étapes correspondantes dans Installer un serveur Minecraft sur Debian et Checklist pour un nouveau serveur root.

Questions fréquentes

Combien de RAM dois-je allouer à mon serveur Minecraft ?
Allouez ce dont le monde a réellement besoin et laissez au moins 1 à 1,5 GB à la JVM hors du heap, ainsi que 512 MB au système d'exploitation. Sur un serveur de 8 GB de RAM, cela donne -Xmx6G. Vanilla avec jusqu'à 10 joueurs se contente de 2G, Paper avec des plugins et 30 joueurs de 4G, un modpack moyen demande 8G à 10G. Au-delà d'environ 12 GB de heap, les pauses de la garbage collection s'allongent au lieu que les performances augmentent.
Pourquoi Xms doit-il être égal à Xmx ?
Si -Xms est inférieur à -Xmx, le heap grandit et se rétracte pendant le fonctionnement. Chaque changement de taille déclenche une garbage collection complète et de nouveaux page faults, ce qui se traduit par des micro-freezes récurrents. Des valeurs identiques, plus -XX:+AlwaysPreTouch, réservent la totalité du heap dès le démarrage. Le démarrage prend quelques secondes de plus, mais le fonctionnement reste régulier.
Quelle est la différence entre un OutOfMemoryError et un processus qui disparaît simplement avec Killed ?
L'OutOfMemoryError vient de la JVM et signifie que le heap défini par -Xmx est plein. Un processus qui se termine sans stacktrace avec Killed ou status=9/KILL a été arrêté par le kernel Linux, parce que la mémoire du système entier était épuisée. Dans le premier cas, -Xmx est trop petit, dans le second trop grand. Le cas kernel se prouve avec dmesg | grep -i "out of memory".
Comment reconnaître une fuite mémoire causée par un plugin ?
Ce qui compte, c'est le heap occupé juste après une garbage collection complète. Vous pouvez la forcer avec jmap -histo:live PID et lire le résultat avec jcmd PID GC.heap_info. Notez la valeur après le démarrage, puis après deux, six et douze heures. Si elle reste stable, le heap est simplement trop petit. Si elle monte régulièrement à nombre de joueurs égal, il y a une fuite. Le plugin spark fournit la même analyse plus confortablement via /spark healthreport --memory et /spark heapsummary.
Est-ce que plus de swap aide contre l'erreur Java heap space ?
Non. Le swap n'agrandit pas le heap, dont la limite supérieure est fixée par -Xmx. Pire encore : un heap paginé sur le disque rend la garbage collection extrêmement lente, une pause de 50 millisecondes devient un gel de 20 secondes. Prévoyez 2 à 4 GB de swap avec vm.swappiness=10 comme tampon contre l'OOM killer, jamais comme extension de mémoire planifiée.
Pourquoi est-ce que je ne trouve pas jcmd et jmap sur mon serveur ?
Ces outils ne sont pas inclus dans les paquets openjdk-XX-jre-headless, mais uniquement dans openjdk-XX-jdk-headless. Si vous exploitez le serveur avec le seul paquet d'exécution, vous devez installer le paquet JDK, par exemple avec apt install openjdk-21-jdk-headless. Tenez compte de la disponibilité des paquets : Debian 13 fournit Java 21 et 25, Debian 12 fournit Java 17.

Minecraft Java JVM Serveurs de jeu Dépannage RAM Garbage Collection Linux