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

Publicado a 15 min de leitura

A mensagem java.lang.OutOfMemoryError: Java heap space não significa automaticamente falta de RAM. Como dimensionar Xmx e Xms corretamente, encontrar fugas de memória e usar swap com sentido.

O servidor corre há três horas, depois bloqueia, a tickrate cai para 2 e na consola aparece:

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

O primeiro reflexo é quase sempre o mesmo: atribuir mais RAM. Em cerca de metade dos casos essa é exatamente a reação errada e, numa parte deles, piora a situação de forma mensurável. Este artigo mostra o que a mensagem significa realmente, como dimensionar -Xmx e -Xms de forma limpa, como distinguir uma fuga de memória de falta real de memória e como reconhecer que a correção se aguentou.

O que a mensagem significa exatamente, e o que não significa

O heap do Java é a área onde a JVM guarda os objetos: chunks carregados, entidades, inventários, dados de plugins. O seu limite superior é definido com -Xmx. Um OutOfMemoryError: Java heap space significa: a JVM quis criar um objeto, o heap estava cheio e a garbage collection não conseguiu libertar espaço suficiente. Isso não diz absolutamente nada sobre a memória livre do sistema operativo. Um servidor com 64 GB de RAM lança a mesma mensagem com igual fiabilidade se estiver definido -Xmx2G e o mundo precisar de 4 GB.

Há que distinguir isto claramente de duas outras falhas, muitas vezes confundidas com a primeira:

  • O processo desaparece sem stacktrace, no log aparece apenas Killed ou, no journal, Main process exited, code=killed, status=9/KILL. Foi o kernel, não a JVM. O OOM killer do Linux atuou porque a memória total do sistema tinha esgotado. Pode confirmar isso com dmesg | grep -i "out of memory", onde encontra então uma linha como Out of memory: Killed process 1337 (java).
  • O Java nem sequer arranca e comunica Error occurred during initialization of VM / Could not reserve enough space for object heap. Nesse caso, -Xmx é maior do que aquilo que o sistema consegue sequer disponibilizar.

Esta distinção é o passo mais importante. Na primeira variante há heap a menos, na segunda e na terceira há heap a mais. Quem confunde os casos mexe no parâmetro errado.

Primeiro medir, depois atribuir

Antes de alterar qualquer valor, precisa de dois números: a memória realmente disponível e o consumo atual.

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

O que é decisivo é MemAvailable, não free. O Linux usa a RAM não utilizada como cache de ficheiros, por isso o valor "free" é quase sempre pequeno e quase sempre irrelevante.

O consumo real do servidor vê-se assim:

ps -o pid,rss,cmd -C java

O valor RSS vem em kilobytes e corresponde à memória que o processo ocupa na RAM. É sempre maior do que -Xmx, e é precisamente neste ponto que a maioria dos guias termina.

Porque "o máximo possível" corre garantidamente mal

Além do heap, a JVM precisa ainda de toda uma série de outras áreas de memória que -Xmx não abrange de todo:

  • Metaspace: as classes carregadas. Num modpack com 300 mods chegam depressa aos 300 a 600 MB.
  • Stacks de threads: cada thread recebe cerca de 1 MB. Chunk workers, threads do Netty, schedulers de plugins, tudo isso soma 100 a 300 MB.
  • Direct Buffers: o Netty processa todo o tráfego de rede através de memória fora do heap. Com muitos jogadores em simultâneo, várias centenas de megabytes são normais.
  • Code cache e estruturas do GC: o compilador JIT e a contabilidade do próprio G1 custam grosso modo 5 a 10 por cento do heap.

Como regra prática fiável vale o seguinte: conte com Xmx mais 1 a 1,5 GB para a JVM, mais pelo menos 512 MB para o sistema operativo. Num modpack com muitos mods, antes Xmx mais 2 GB.

Num servidor com 8 GB de RAM isso significa -Xmx6G e não -Xmx8G. Quem atribuir 8 deixa de ter o erro de heap e passa a ter algo pior: um processo abatido pelo kernel sem aviso prévio, a meio da gravação do mundo. O erro de heap é uma falha limpa e documentada. O OOM kill pode deixar para trás ficheiros de região corrompidos.

Há ainda um segundo motivo contra a atribuição máxima: um heap demasiado grande torna a garbage collection mais lenta. O G1 tem de percorrer mais memória, as pausas de um mixed GC ficam mais longas e um engasgo ocasional transforma-se num freeze bem percetível. Acima de cerca de 12 GB, no Minecraft a relação costuma inverter-se para pior. Quem precisar de mais deve dividir o mundo em vez de aumentar o heap.

Regras práticas por número de jogadores e modpack

Estes valores são pontos de partida, não leis da natureza. Pressupõem um mundo de tamanho normal e pré-geração ativa.

Tipo de servidorJogadoresXmxRAM no sistema
Vanilla ou Paper, sem pluginsaté 102G4 GB
Paper com 15 a 30 plugins10 a 304G8 GB
Paper, grande conjunto de plugins, base de dados30 a 806G a 8G12 a 16 GB
Modpack leve, até 120 modsaté 106G8 GB
Modpack médio, 150 a 250 modsaté 208G a 10G16 GB
Modpack pesado, a partir de 300 modsaté 2010G a 12G16 a 24 GB
Proxy (Velocity, BungeeCord)qualquer512M a 1G2 GB

Duas notas sobre isto. Primeiro, nos modpacks a necessidade de heap escala quase exclusivamente com o número de mods e o tamanho do mundo, quase nada com o número de jogadores. Segundo, view-distance no server.properties é a alavanca mais eficaz de todas: baixar de 10 para 8 poupa muitas vezes mais memória do que 2 GB adicionais de heap, porque o número de chunks carregados cresce de forma quadrática com a distância de visão. Definir simulation-distance em 6 atua adicionalmente sobre a carga de CPU.

Xms igual a Xmx: o aquecimento

-Xms define com quanto heap a JVM arranca. Se aí estiver um valor menor do que em -Xmx, o heap cresce aos poucos durante a operação. Cada aumento significa uma recolha completa da garbage collection e novos page faults no sistema operativo e, como o G1 também deixa o heap voltar a encolher, o jogo repete-se. É precisamente daqui que vêm os famosos engasgos de poucos em poucos minutos nas primeiras horas depois do arranque.

Defina -Xms sempre igual a -Xmx. Com -XX:+AlwaysPreTouch em complemento, a JVM toca uma vez em cada página de memória do heap logo no arranque. O arranque demora assim 2 a 15 segundos mais, consoante o tamanho do heap, mas durante o jogo deixam de ocorrer page faults. O efeito secundário agradável: se -Xmx estiver escolhido demasiado alto, isso nota-se por regra logo no arranque e não só três horas mais tarde, a meio do jogo.

Não confie, porém, num teste rápido com -version. Medido na prática: java -Xms4G -Xmx4G -XX:+AlwaysPreTouch -version correu sem qualquer problema num ambiente limitado a 2 GB, porque a chamada termina antes de o heap estar realmente a ser usado. Uma prova limpa é apenas o arranque real do servidor, seguido da observação de ps -o rss= -C java.

Um comando de arranque comprovado para o Paper fica então assim (as chamadas flags do 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, o PaperMC recomenda valores ajustados: G1NewSizePercent=40, G1MaxNewSizePercent=50, G1HeapRegionSize=16M, G1ReservePercent=15 e InitiatingHeapOccupancyPercent=20.

As duas últimas linhas são o verdadeiro ganho e faltam em quase todos os guias. -XX:+HeapDumpOnOutOfMemoryError escreve, no momento do crash, uma imagem completa da memória, com a qual é possível comprovar a causa mais tarde. -XX:+ExitOnOutOfMemoryError termina a JVM de imediato, em vez de a deixar a correr num estado meio morto no qual os jogadores se ligam e perdem progresso. Em conjunto com Restart=on-failure no ficheiro da unit, daí resulta um reinício limpo, ver a propósito iniciar automaticamente um servidor Minecraft e criar um serviço systemd.

Conte com o seguinte: o diretório de dumps tem de existir e ter pelo menos tanto espaço quanto -Xmx. Um heap de 8 GB gera um ficheiro .hprof com 8 GB. Se o disco ficar cheio a seguir, tem um segundo problema, ver disco cheio no Linux.

Versão do Java e diferenças entre sistemas

A JVM que utiliza altera o comportamento de forma percetível:

  • O Java 8 usa por predefinição o collector paralelo, não o G1. Aqui surge também a variante java.lang.OutOfMemoryError: GC overhead limit exceeded, que significa que mais de 98 por cento do tempo foi passado na garbage collection. O registo do GC faz-se através de -XX:+PrintGCDetails -Xloggc:gc.log.
  • A partir do Java 9, o G1 é a predefinição em todas as máquinas com pelo menos dois núcleos e 1792 MB de RAM. O registo passa pelo novo unified logging: -Xlog:gc*:file=logs/gc.log:time,uptime:filecount=5,filesize=10M. As flags antigas são aqui rejeitadas e o servidor não arranca.
  • Situação dos pacotes: o Debian 13 fornece openjdk-21-jre-headless e openjdk-25-jre-headless, mas nenhum Java 17. O Debian 12 fornece o Java 17, mas nem o 21 nem o 25. O Ubuntu 22.04 e 24.04 têm 8, 11, 17, 21 e 25. Quem precisar de uma versão específica que a distribuição não conheça recorre ao repositório do Adoptium (Temurin 8 a 26 para trixie, bookworm, noble e jammy). Detalhes em instalar o Java 21 no Debian e instalar o Java 17 no Debian.

A armadilha que apanha quase toda a gente: as ferramentas de diagnóstico jcmd, jmap, jstat e jstack não vêm incluídas nos pacotes jre-headless. Estão no openjdk-XX-jdk-headless. Quem opera o servidor com o JRE fica sem ferramentas na hora da verdade:

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

A primeira linha vale para Debian e Ubuntu, a segunda para AlmaLinux, Rocky Linux e Oracle Linux. Depois disso, jcmd e jmap ficam em /usr/bin/. Verifique isso com command -v jcmd e não com which jcmd: na instalação mínima da família Red Hat falta o which e, no EL 10, está de qualquer forma descontinuado.

Detetar fugas de memória causadas por plugins

Uma fuga de memória não tem o mesmo aspeto que a simples falta de memória. A diferença está na evolução: com um heap demasiado pequeno, o consumo estabiliza num nível alto depois do arranque e fica por aí. Numa fuga, o valor de base continua a subir após cada garbage collection completa. É precisamente esse valor de base o indicador que interessa.

Pode ser consultado diretamente. Primeiro determinar o ID do processo, depois forçar uma recolha completa e observar o heap:

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

Nota importante: jmap -histo:live desencadeia internamente uma recolha completa e funciona também quando, como acima, está definido -XX:+DisableExplicitGC. O comando óbvio jcmd PID GC.run não faz nada nesta constelação, porque passa por System.gc() e foi exatamente isso que ficou desativado. Pela experiência, isto custa meia hora de confusão.

Anote o valor logo depois do arranque, depois ao fim de duas, seis e doze horas. Se o valor após a recolha completa subir continuamente sem que haja mais jogadores online, trata-se de uma fuga. A lista de classes do histograma mostra normalmente logo o caminho: ItemStack aos milhares, CraftPlayer de jogadores há muito desligados ou um objeto com o nome de pacote de um plugin.

É bastante mais cómodo fazer isto com o plugin spark, disponível para Paper, Fabric e Forge:

  • /spark healthreport --memory mostra de relance a utilização do heap, o comportamento do GC e as áreas fora do heap.
  • /spark heapsummary gera uma lista de classes ordenada por consumo de memória, sem escrever um dump com vários gigabytes.
  • /spark gc mostra a frequência e a duração das recolhas.

Se a suspeita recair sobre um plugin concreto, a contraprova é simples: remover o plugin, deixar o servidor a correr 24 horas e medir de novo o valor de base. Candidatos clássicos são plugins que guardam dados de jogadores em maps e não limpam nada quando estes se desligam, bem como tudo o que tem que ver com edição de mundo e mantém um histórico de anulação ilimitado.

Swap: boia de salvação, não ampliação de memória

O swap e um heap do Java são inimigos naturais. A garbage collection percorre regularmente grandes partes do heap. Se apenas uma fração disso estiver no disco, uma pausa de 50 milissegundos passa a ser de 20 segundos e o servidor é dado como bloqueado. Nunca conte o swap para o tamanho do heap.

Ainda assim, deve existir swap, só que pequeno e pouco solicitado. Funciona como amortecedor para que picos curtos não desencadeiem logo o OOM killer e acolhe páginas raramente usadas de outros serviços. Recomendam-se 2 a 4 GB e uma baixa tendência para paginar:

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

De forma permanente, isso define-se em /etc/sysctl.d/99-swappiness.conf. A configuração em detalhe está em configurar swap e evitar out of memory.

Um caso especial merece atenção: -XX:+AlwaysPreTouch em combinação com pouca RAM. Como no arranque é tocada cada página do heap, a parte excedente vai imediatamente para o swap. O servidor até arranca, mas fica inutilizavelmente lento desde o primeiro tick. Se um servidor passar a arrancar extremamente devagar depois de ativar o PreTouch, a culpa é de -Xmx estar demasiado alto, não do PreTouch.

Como limite rígido adicional pode definir MemoryMax na unit do systemd, por exemplo MemoryMax=7G com 8 GB de RAM. Assim, em caso de descarrilamento, o kernel atinge de forma dirigida o processo do Minecraft e não a base de dados ou o acesso SSH.

Como reconhecer que está mesmo resolvido

Um reinício sem crash imediato não é prova de nada. Verifique antes estes cinco pontos após 24 horas de operação sob carga normal:

  1. jcmd PID GC.heap_info: o heap ocupado logo depois de uma recolha completa deve ficar claramente abaixo de 70 por cento de -Xmx e manter-se estável ao longo do tempo.
  2. ps -o rss= -C java: o valor deve estabilizar em cerca de Xmx mais 1 a 1,5 GB e não continuar a subir. Se subir enquanto o heap se mantém estável, a fuga está fora do heap, tipicamente no Metaspace ou nos Direct Buffers.
  3. free -h: available nunca deve descer abaixo de cerca de 500 MB.
  4. swapon --show: a quantidade de swap ocupada deve manter-se perto de zero.
  5. O log do GC: recolhas completas ("Pause Full") não devem praticamente ocorrer e as pausas habituais devem manter-se abaixo de 200 milissegundos. Vários full GCs em série, uns atrás dos outros, são o sinal certo do próximo OutOfMemoryError, muitas vezes um quarto de hora antes.

Além disso, /tps ou /spark tps mostram dentro do jogo se a tickrate se mantém estável em 20,0. Um servidor saudável do ponto de vista da memória mantém este valor mesmo ao fim de horas.

Quando o Java nem sequer arranca

Quatro mensagens, na sua formulação literal, que surgem tipicamente ao ajustar os valores de memória:

  • Invalid maximum heap size: -Xmx8GB é a gralha mais frequente. A unidade chama-se G, não GB. São permitidos k, m e g, em maiúsculas ou minúsculas.
  • Initial heap size set to a larger value than the maximum heap size significa que -Xms é maior do que -Xmx, normalmente porque ao copiar só se ajustou um dos dois valores.
  • Could not reserve enough space for object heap significa que a atribuição excede a memória disponível. Numa JVM de 32 bits o limite fica sempre pouco abaixo dos 4 GB, independentemente da RAM existente. Verifique com java -version, onde tem de constar 64-Bit Server VM.
  • Unrecognized VM option 'UseG1GC' ou semelhante aponta para uma JVM demasiado antiga ou errada. Algumas flags experimentais exigem obrigatoriamente um -XX:+UnlockExperimentalVMOptions à frente, e isso antes da flag em causa na linha de comandos.

Com que valores a JVM realmente trabalha no fim, isso pode ser verificado a qualquer momento. Sem servidor a correr, através dos valores predefinidos:

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

Dois pormenores aí são intencionais. O 2>/dev/null engole o banner de versão que a JVM escreve na saída de erro e que, de outra forma, passaria ao lado do grep e apareceria sem filtro no resultado. E grep -w MaxHeapSize em vez de grep -i maxheapsize devolve mesmo apenas uma linha: a pesquisa difusa encontra também SoftMaxHeapSize e quem espera um único valor lê facilmente o errado.

E no processo em execução, através das flags realmente ativas:

jcmd PID VM.flags

Esta é a forma mais fiável de descobrir a situação surpreendentemente comum em que um script de arranque foi de facto alterado, mas o servidor continua a correr com os valores antigos de um segundo ficheiro de script.


Em resumo: meça primeiro se o heap é sequer o problema. Atribua depois tanto quanto o mundo precisa e deixe pelo menos 1,5 GB para a JVM e para o sistema. Defina -Xms igual a -Xmx, ative o heap dump para a hora da verdade e observe o valor de base após a recolha completa ao longo de várias horas. A diferença entre "funciona" e "funciona de forma estável" está exatamente neste último passo.

Para a configuração de base do próprio servidor encontra os passos adequados em instalar um servidor Minecraft no Debian e checklist para um novo servidor root.

Perguntas frequentes

Quanta RAM devo atribuir ao meu servidor Minecraft?
Atribua tanto quanto o mundo realmente precisa e deixe pelo menos 1 a 1,5 GB para a JVM fora do heap, além de 512 MB para o sistema operativo. Num servidor com 8 GB de RAM isso significa -Xmx6G. Vanilla com até 10 jogadores fica-se por 2G, Paper com plugins e 30 jogadores por 4G, um modpack médio precisa de 8G a 10G. Acima de cerca de 12 GB de heap, as pausas da garbage collection ficam mais longas em vez de o desempenho aumentar.
Porque deve Xms ser igual a Xmx?
Se -Xms for menor do que -Xmx, o heap cresce e encolhe durante a operação. Cada alteração de tamanho provoca uma garbage collection completa e novos page faults, o que se nota como engasgos recorrentes. Valores iguais mais -XX:+AlwaysPreTouch reservam todo o heap logo no arranque. O arranque demora assim alguns segundos mais, mas a operação mantém-se uniforme.
Qual é a diferença entre um OutOfMemoryError e um processo que simplesmente desaparece com Killed?
O OutOfMemoryError vem da JVM e significa: o heap definido com -Xmx está cheio. Um processo que termina sem stacktrace com Killed ou status=9/KILL foi encerrado pelo kernel do Linux, porque a memória de todo o sistema tinha esgotado. No primeiro caso -Xmx é demasiado pequeno, no segundo demasiado grande. O caso do kernel comprova-se com dmesg | grep -i "out of memory".
Como reconheço uma fuga de memória causada por um plugin?
O que conta é o heap ocupado logo depois de uma garbage collection completa. Pode forçá-la com jmap -histo:live PID e ler o valor com jcmd PID GC.heap_info. Anote o valor após o arranque e ainda ao fim de duas, seis e doze horas. Se se mantiver estável, o heap está apenas demasiado pequeno. Se subir continuamente com o mesmo número de jogadores, existe uma fuga. O plugin spark fornece a mesma análise de forma mais cómoda através de /spark healthreport --memory e /spark heapsummary.
Mais swap ajuda contra o erro Java heap space?
Não. O swap não aumenta o heap, porque o limite superior deste está em -Xmx. Pior ainda: heap paginado para o disco torna a garbage collection extremamente lenta, uma pausa de 50 milissegundos passa a 20 segundos de freeze. Fazem sentido 2 a 4 GB de swap com vm.swappiness=10 como amortecedor contra o OOM killer, nunca como ampliação de memória planeada.
Porque não encontro o jcmd e o jmap no meu servidor?
Estas ferramentas não estão incluídas nos pacotes openjdk-XX-jre-headless, apenas no openjdk-XX-jdk-headless. Quem opera o servidor só com o pacote de runtime tem de instalar adicionalmente o pacote JDK, por exemplo com apt install openjdk-21-jdk-headless. Tenha em conta a situação dos pacotes: o Debian 13 fornece Java 21 e 25, o Debian 12 fornece Java 17.

Minecraft Java JVM Servidor de jogos Resolução de problemas Memória RAM Garbage Collection Linux