Servidor Minecraft: solucionar "java.lang.OutOfMemoryError: Java heap space"

Publicado el 15 min de lectura

El mensaje java.lang.OutOfMemoryError: Java heap space no significa automáticamente que falte RAM. Cómo dimensionar Xmx y Xms correctamente, encontrar fugas de memoria y usar el swap con criterio.

El servidor lleva tres horas funcionando, de pronto se congela, la tasa de ticks cae a 2 y en la consola aparece esto:

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

El primer reflejo es casi siempre el mismo: asignar más RAM. En aproximadamente la mitad de los casos esa es justo la reacción equivocada, y en una parte de ellos empeora la situación de forma medible. Este artículo explica qué significa realmente el mensaje, cómo dimensionar -Xmx y -Xms con criterio, cómo distinguir una fuga de memoria de una falta real de memoria y en qué notas que la solución ha aguantado.

Qué significa exactamente el mensaje, y qué no

El heap de Java es la zona donde la JVM guarda los objetos: chunks cargados, entidades, inventarios, datos de plugins. Su límite superior lo fijas con -Xmx. Un OutOfMemoryError: Java heap space significa que la JVM quiso crear un objeto, el heap estaba lleno y la garbage collection no consiguió liberar espacio suficiente. Eso no dice nada sobre la memoria libre del sistema operativo. Un servidor con 64 GB de RAM lanza ese mismo mensaje con idéntica fiabilidad si tiene -Xmx2G y el mundo necesita 4 GB.

Hay que distinguir esto con precisión de otros dos fallos que se confunden con frecuencia:

  • El proceso desaparece sin stacktrace, en el log solo pone Killed o en el journal Main process exited, code=killed, status=9/KILL. Ahí actuó el kernel, no la JVM. El OOM killer de Linux intervino porque se había agotado la memoria completa del sistema. Puedes comprobarlo con dmesg | grep -i "out of memory", donde aparecerá entonces una línea como Out of memory: Killed process 1337 (java).
  • Java ni siquiera arranca y avisa con Error occurred during initialization of VM / Could not reserve enough space for object heap. En ese caso -Xmx es mayor de lo que el sistema puede llegar a ofrecer.

Esta distinción es el paso más importante. En la primera variante falta heap, en la segunda y la tercera sobra. Quien confunde los casos acaba girando el tornillo equivocado.

Primero medir, después asignar

Antes de cambiar ningún valor necesitas dos cifras: la memoria realmente disponible y el consumo actual.

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

Lo decisivo es MemAvailable, no free. Linux aprovecha la RAM sin usar como caché de archivos, por eso "free" es casi siempre un valor pequeño y casi siempre irrelevante.

El consumo real del servidor lo ves así:

ps -o pid,rss,cmd -C java

El valor RSS viene en kilobytes y es la memoria que el proceso ocupa en la RAM. Es siempre mayor que -Xmx, y ese es justo el punto en el que se detienen la mayoría de las guías.

Por qué asignar "todo lo posible" sale mal con toda seguridad

Además del heap, la JVM necesita toda una serie de zonas de memoria adicionales que -Xmx no cubre en absoluto:

  • Metaspace: las clases cargadas. Con un modpack de 300 mods son fácilmente entre 300 y 600 MB.
  • Thread stacks: cada hilo recibe alrededor de 1 MB. Workers de chunks, hilos de Netty, schedulers de plugins, todo eso suma entre 100 y 300 MB.
  • Direct buffers: Netty gestiona todo el tráfico de red con memoria situada fuera del heap. Con muchos jugadores simultáneos, varios cientos de megabytes son lo normal.
  • Code cache y estructuras del GC: el compilador JIT y la propia contabilidad interna de G1 cuestan a grandes rasgos entre un 5 y un 10 por ciento del heap.

Como regla práctica fiable: cuenta con Xmx más 1 a 1,5 GB para la JVM, más al menos 512 MB para el sistema operativo. Con un modpack de muchos mods, mejor Xmx más 2 GB.

En un servidor con 8 GB de RAM eso significa -Xmx6G y no -Xmx8G. Quien asigna 8 ya no recibe un error de heap, sino algo peor: un proceso al que el kernel mata sin previo aviso, justo mientras se guarda el mundo. El error de heap es un fallo limpio y documentado. El OOM kill puede dejar atrás archivos de región corruptos.

Hay un segundo motivo en contra de asignar el máximo: un heap demasiado grande vuelve más lenta la garbage collection. G1 tiene que recorrer más memoria, las pausas de un mixed GC se alargan y lo que era un tirón ocasional se convierte en un freeze perceptible. Por encima de unos 12 GB, en Minecraft la relación suele volverse negativa. Quien necesite más debería repartir el mundo en lugar de agrandar el heap.

Reglas prácticas según número de jugadores y modpack

Estos valores son puntos de partida, no leyes naturales. Parten de un mundo de tamaño normal y de un pregenerado activo.

Tipo de servidorJugadoresXmxRAM del sistema
Vanilla o Paper, sin pluginshasta 102G4 GB
Paper con 15 a 30 plugins10 a 304G8 GB
Paper, suite grande de plugins, base de datos30 a 806G a 8G12 a 16 GB
Modpack ligero, hasta 120 modshasta 106G8 GB
Modpack medio, 150 a 250 modshasta 208G a 10G16 GB
Modpack pesado, a partir de 300 modshasta 2010G a 12G16 a 24 GB
Proxy (Velocity, BungeeCord)cualquiera512M a 1G2 GB

Dos apuntes al respecto. Primero, en los modpacks la necesidad de heap escala casi en exclusiva con el número de mods y el tamaño del mundo, apenas con el número de jugadores. Segundo, view-distance en server.properties es la palanca más eficaz que existe: bajar de 10 a 8 ahorra a menudo más memoria que 2 GB extra de heap, porque el número de chunks cargados crece de forma cuadrática con la distancia de visión. Poner simulation-distance en 6 actúa además sobre la carga de CPU.

Xms igual a Xmx: el calentamiento

-Xms determina con cuánto heap arranca la JVM. Si ahí hay un valor menor que en -Xmx, el heap va creciendo poco a poco durante el funcionamiento. Cada ampliación supone una recolección completa de la garbage collection y nuevos page faults en el sistema operativo, y como G1 también deja que el heap vuelva a encogerse, el juego se repite. De ahí vienen justamente los famosos tirones cada pocos minutos durante las primeras horas tras el arranque.

Pon -Xms siempre igual a -Xmx. Si lo completas con -XX:+AlwaysPreTouch, la JVM toca una vez cada página de memoria del heap durante el arranque. El inicio tarda así entre 2 y 15 segundos más según el tamaño del heap, pero a cambio ya no se producen page faults durante la partida. El efecto secundario agradable: si -Xmx está elegido demasiado alto, por lo general eso se nota ya en el arranque y no tres horas más tarde en plena partida.

Pero no te fíes para ello de una prueba rápida con -version. Medido en la práctica: java -Xms4G -Xmx4G -XX:+AlwaysPreTouch -version se ejecutó sin problemas en un entorno limitado a 2 GB, porque la llamada termina antes de que el heap esté realmente en uso. La única prueba limpia es el arranque real del servidor con la observación posterior de ps -o rss= -C java.

Un comando de arranque probado para Paper queda entonces así (los llamados flags de 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

A partir de 12 GB de heap, PaperMC recomienda valores adaptados: G1NewSizePercent=40, G1MaxNewSizePercent=50, G1HeapRegionSize=16M, G1ReservePercent=15 e InitiatingHeapOccupancyPercent=20.

Las dos últimas líneas son la verdadera ganancia y faltan en casi todas las guías. -XX:+HeapDumpOnOutOfMemoryError escribe una imagen completa de la memoria en el momento de la caída, con la que después se puede demostrar la causa. -XX:+ExitOnOutOfMemoryError termina la JVM de inmediato en lugar de dejarla seguir en un estado medio muerto, en el que los jugadores se conectan y pierden progreso. Junto con Restart=on-failure en el archivo de la unidad, de ahí sale un reinicio limpio, véase al respecto Iniciar un servidor Minecraft automáticamente y Crear un servicio systemd.

Tenlo en cuenta: el directorio de dumps tiene que existir y disponer de al menos tanto espacio como -Xmx. Un heap de 8 GB genera un archivo .hprof de 8 GB. Si después el disco se queda lleno, tienes un segundo problema, véase Disco lleno en Linux.

Versión de Java y diferencias entre sistemas

La JVM que uses cambia el comportamiento de forma notable:

  • Java 8 usa por defecto el collector paralelo, no G1. Aquí aparece también la variante java.lang.OutOfMemoryError: GC overhead limit exceeded, que significa que más del 98 por ciento del tiempo se ha pasado en la garbage collection. El registro del GC se activa con -XX:+PrintGCDetails -Xloggc:gc.log.
  • A partir de Java 9, G1 es el estándar en todas las máquinas con al menos dos núcleos y 1792 MB de RAM. El registro va por el nuevo Unified Logging: -Xlog:gc*:file=logs/gc.log:time,uptime:filecount=5,filesize=10M. Aquí los flags antiguos se rechazan y el servidor no arranca.
  • Situación de los paquetes: Debian 13 trae openjdk-21-jre-headless y openjdk-25-jre-headless, pero no Java 17. Debian 12 trae Java 17, pero ni 21 ni 25. Ubuntu 22.04 y 24.04 tienen 8, 11, 17, 21 y 25. Quien necesite una versión concreta que la distribución no conozca, recurre al repositorio de Adoptium (Temurin 8 a 26 para trixie, bookworm, noble y jammy). Detalles en Instalar Java 21 en Debian y Instalar Java 17 en Debian.

La trampa en la que cae casi todo el mundo: las herramientas de diagnóstico jcmd, jmap, jstat y jstack no vienen incluidas en los paquetes jre-headless. Están dentro de openjdk-XX-jdk-headless. Quien gestiona el servidor solo con el JRE se queda sin herramientas justo cuando las necesita:

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

La primera línea vale para Debian y Ubuntu, la segunda para AlmaLinux, Rocky Linux y Oracle Linux. Después, jcmd y jmap quedan en /usr/bin/. Compruébalo con command -v jcmd y no con which jcmd: en la instalación mínima de la familia Red Hat falta which, y en EL 10 está obsoleto de todas formas.

Detectar fugas de memoria causadas por plugins

Una fuga de memoria tiene un aspecto distinto al de una simple falta de memoria. La diferencia está en la evolución: con un heap demasiado pequeño, el consumo se estabiliza tras el arranque en un nivel alto y se queda ahí. Con una fuga, el valor base sigue subiendo después de cada garbage collection completa. Ese valor base es precisamente la cifra que importa.

Se puede consultar directamente. Primero averigua el ID del proceso, después fuerza una recolección completa y mira el heap:

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

Aviso importante: jmap -histo:live dispara internamente una recolección completa y funciona también cuando, como arriba, está puesto -XX:+DisableExplicitGC. El comando más obvio, jcmd PID GC.run, no hace nada en esa combinación, porque va por System.gc() y eso es exactamente lo que se ha desactivado. Por experiencia, eso cuesta media hora de confusión.

Anota el valor justo después del arranque, y luego a las dos, seis y doce horas. Si el valor tras la recolección completa sube de forma constante sin que haya más jugadores conectados, es una fuga. La lista de clases del histograma suele indicar ya por dónde van los tiros: cantidades enormes de ItemStack, objetos CraftPlayer de jugadores que salieron hace mucho o un objeto con el nombre de paquete de un plugin.

Resulta bastante más cómodo con el plugin spark, disponible para Paper, Fabric y Forge:

  • /spark healthreport --memory muestra de un vistazo el uso del heap, el comportamiento del GC y las zonas fuera del heap.
  • /spark heapsummary genera una clasificación de las clases por consumo de memoria, sin escribir para ello un dump de varios gigabytes.
  • /spark gc muestra la frecuencia y la duración de las recolecciones.

Cuando la sospecha recae sobre un plugin concreto, la comprobación es sencilla: quita el plugin, deja el servidor funcionando 24 horas y vuelve a medir el valor base. Los candidatos clásicos son los plugins que guardan datos de jugadores en maps y no los limpian al desconectarse, además de todo lo que tiene que ver con la edición del mundo y mantiene un historial de deshacer ilimitado.

Swap: salvavidas, no ampliación de memoria

El swap y un heap de Java son enemigos naturales. La garbage collection recorre con regularidad grandes partes del heap. Si aunque sea una fracción de él está en el disco, una pausa de 50 milisegundos se convierte en una de 20 segundos y el servidor pasa por congelado. No cuentes nunca el swap dentro del tamaño del heap.

Aun así, conviene que haya swap, solo que pequeño y poco activo. Actúa como colchón para que los picos breves no disparen enseguida al OOM killer, y recoge páginas poco usadas de otros servicios. Son recomendables entre 2 y 4 GB y una tendencia baja al intercambio:

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

De forma permanente se define en /etc/sysctl.d/99-swappiness.conf. La configuración en detalle está en Configurar swap y evitar el Out of Memory.

Un caso especial merece atención: -XX:+AlwaysPreTouch combinado con poca RAM. Como en el arranque se toca cada página del heap, la parte sobrante se va de inmediato al swap. El servidor arranca, sí, pero desde el primer tick va tan lento que resulta inservible. Si un servidor arranca de repente con una lentitud extrema después de activar PreTouch, la culpa es de un -Xmx demasiado grande, no de PreTouch.

Como límite duro puedes fijar además MemoryMax en la unidad de systemd, por ejemplo MemoryMax=7G con 8 GB de RAM. Así, si algo se desmadra, el kernel golpea justo al proceso de Minecraft y no a la base de datos ni al acceso SSH.

Cómo sabes que está realmente resuelto

Un reinicio sin caída inmediata no demuestra nada. Comprueba en su lugar estos cinco puntos después de 24 horas de funcionamiento con carga normal:

  1. jcmd PID GC.heap_info: el heap ocupado justo después de una recolección completa debería quedar claramente por debajo del 70 por ciento de -Xmx y mantenerse estable con el tiempo.
  2. ps -o rss= -C java: el valor debería estabilizarse en torno a Xmx más 1 a 1,5 GB y no seguir subiendo. Si sube mientras el heap se mantiene estable, la fuga está fuera del heap, normalmente en el metaspace o en los direct buffers.
  3. free -h: available no debería bajar nunca de unos 500 MB.
  4. swapon --show: la cantidad de swap ocupada debería mantenerse cerca de cero.
  5. El log del GC: las recolecciones completas ("Pause Full") no deberían aparecer prácticamente nunca, y las pausas habituales tienen que quedarse por debajo de 200 milisegundos. Una serie de Full GC seguidos es el aviso seguro del siguiente OutOfMemoryError, a menudo un cuarto de hora antes.

Además, /tps o bien /spark tps muestran dentro del juego si la tasa de ticks se mantiene estable en 20,0. Un servidor sano en cuanto a memoria conserva ese valor también después de horas.

Cuando Java ni siquiera arranca

Cuatro mensajes literales que suelen aparecer al ajustar los valores de memoria:

  • Invalid maximum heap size: -Xmx8GB es la errata más habitual. La unidad se escribe G, no GB. Se admiten k, m y g, en mayúscula o en minúscula.
  • Initial heap size set to a larger value than the maximum heap size significa que -Xms es mayor que -Xmx, casi siempre porque al copiar solo se ajustó uno de los dos valores.
  • Could not reserve enough space for object heap significa que la asignación supera la memoria disponible. En una JVM de 32 bits el tope está en algo menos de 4 GB en cualquier caso, con independencia de la RAM instalada. Compruébalo con java -version, ahí tiene que aparecer 64-Bit Server VM.
  • Unrecognized VM option 'UseG1GC' o similar apunta a una JVM demasiado antigua o equivocada. Algunos flags experimentales necesitan obligatoriamente un -XX:+UnlockExperimentalVMOptions por delante, y además antes del flag correspondiente en la línea de comandos.

Con qué valores trabaja realmente la JVM al final se puede verificar en cualquier momento. Sin servidor en marcha, a través de los valores por defecto:

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

Dos pequeños detalles ahí son intencionados. El 2>/dev/null se traga el banner de versión que la JVM escribe en la salida de error y que, de lo contrario, acabaría en la salida sin pasar por el grep. Y grep -w MaxHeapSize en lugar de grep -i maxheapsize devuelve realmente una sola línea: la búsqueda difusa encuentra además SoftMaxHeapSize, y quien espera un único valor lee entonces con facilidad el equivocado.

Y en el proceso en marcha, a través de los flags realmente activos:

jcmd PID VM.flags

Es la vía más fiable para destapar la situación, sorprendentemente frecuente, de que se ha modificado un script de arranque pero el servidor sigue funcionando con los valores antiguos de un segundo archivo de script.


En resumen: mide primero si el heap es de verdad el problema. Asigna después tanto como necesite el mundo y deja libres al menos 1,5 GB para la JVM y el sistema. Pon -Xms igual a -Xmx, activa el heap dump para cuando llegue el problema y observa el valor base tras la recolección completa a lo largo de varias horas. La diferencia entre "funciona" y "funciona de forma estable" está justamente en ese último paso.

Para la configuración básica del servidor en sí encontrarás los pasos adecuados en Instalar un servidor Minecraft en Debian y Lista de comprobación para un servidor root nuevo.

Preguntas frecuentes

¿Cuánta RAM debo asignar a mi servidor Minecraft?
Asigna tanto como necesite realmente el mundo y deja libres al menos 1 a 1,5 GB para la JVM fuera del heap, además de 512 MB para el sistema operativo. En un servidor con 8 GB de RAM eso significa -Xmx6G. Vanilla con hasta 10 jugadores se apaña con 2G, Paper con plugins y 30 jugadores con 4G, y un modpack medio necesita de 8G a 10G. Por encima de unos 12 GB de heap, las pausas de la garbage collection se alargan en lugar de subir el rendimiento.
¿Por qué Xms tiene que ser igual a Xmx?
Si -Xms es menor que -Xmx, el heap crece y se encoge durante el funcionamiento. Cada cambio de tamaño provoca una garbage collection completa y nuevos page faults, lo que se nota como tirones recurrentes. Valores iguales más -XX:+AlwaysPreTouch reservan el heap completo ya en el arranque. El inicio tarda unos segundos más, pero a cambio el funcionamiento se mantiene uniforme.
¿Qué diferencia hay entre un OutOfMemoryError y un proceso que simplemente desaparece con Killed?
El OutOfMemoryError viene de la JVM y significa que el heap definido con -Xmx está lleno. Un proceso que termina sin stacktrace con Killed o status=9/KILL lo ha matado el kernel de Linux, porque se había agotado la memoria de todo el sistema. En el primer caso -Xmx es demasiado pequeño, en el segundo demasiado grande. El caso del kernel se demuestra con dmesg | grep -i "out of memory".
¿Cómo reconozco una fuga de memoria causada por un plugin?
Lo determinante es el heap ocupado justo después de una garbage collection completa. Puedes forzarla con jmap -histo:live PID y leer el valor con jcmd PID GC.heap_info. Anota la cifra tras el arranque y luego a las dos, seis y doce horas. Si se mantiene estable, el heap solo es demasiado pequeño. Si sube de forma constante con el mismo número de jugadores, hay una fuga. El plugin spark ofrece el mismo análisis de forma más cómoda con /spark healthreport --memory y /spark heapsummary.
¿Sirve de algo más swap contra el error Java heap space?
No. El swap no agranda el heap, porque su límite superior lo marca -Xmx. Peor aún: un heap paginado al disco vuelve la garbage collection extremadamente lenta, y una pausa de 50 milisegundos se convierte en 20 segundos de freeze. Lo razonable son de 2 a 4 GB de swap con vm.swappiness=10 como colchón frente al OOM killer, nunca como ampliación de memoria planificada.
¿Por qué no encuentro jcmd ni jmap en mi servidor?
Estas herramientas no vienen incluidas en los paquetes openjdk-XX-jre-headless, sino solo en openjdk-XX-jdk-headless. Quien gestiona el servidor con el paquete de solo ejecución tiene que instalar después el paquete del JDK, por ejemplo con apt install openjdk-21-jdk-headless. Ten en cuenta la situación de los paquetes: Debian 13 trae Java 21 y 25, Debian 12 trae Java 17.

Minecraft Java JVM Servidores de juegos Resolución de problemas Memoria RAM Garbage Collection Linux