Minecraft-сервер: как устранить "java.lang.OutOfMemoryError: Java heap space"
Сообщение java.lang.OutOfMemoryError: Java heap space не всегда означает нехватку RAM. Как правильно рассчитать Xmx и Xms, найти утечку памяти и разумно применять swap.
Сервер работает три часа, потом зависает, tickrate падает до 2, а в консоли появляется:
[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(...)
Первая реакция почти всегда одна и та же: выделить больше RAM. Примерно в половине случаев это ровно то, чего делать не следует, а в части этих случаев ситуация становится измеримо хуже. В статье разобрано, что на самом деле означает это сообщение, как аккуратно рассчитать -Xmx и -Xms, как отличить утечку памяти от настоящей нехватки памяти и по каким признакам видно, что исправление действительно помогло.
Что означает это сообщение, а что нет
Java-heap — это область, в которой JVM размещает объекты: загруженные чанки, сущности, инвентари, данные плагинов. Его верхнюю границу задаёт -Xmx. Сообщение OutOfMemoryError: Java heap space означает следующее: JVM попыталась создать объект, heap оказался заполнен, а сборка мусора не смогла освободить достаточно места. О свободной памяти операционной системы это не говорит ничего. Сервер с 64 ГБ RAM выдаёт такое сообщение ровно так же надёжно, если задано -Xmx2G, а миру нужно 4 ГБ.
От этого следует чётко отличать два других отказа, которые часто путают:
- Процесс исчезает без stacktrace, в логе остаётся только
Killed, а в журналеMain process exited, code=killed, status=9/KILL. Это сработало ядро, а не JVM. Linux-OOM-killer убил процесс, потому что закончилась вся память системы. Проверить это можно командойdmesg | grep -i "out of memory", там будет строка видаOut of memory: Killed process 1337 (java). - Java вообще не запускается и сообщает
Error occurred during initialization of VM / Could not reserve enough space for object heap. Значит,-Xmxбольше того, что система в принципе может выделить.
Это разграничение и есть самый важный шаг. В первом случае heap слишком мал, во втором и третьем слишком велик. Кто путает эти случаи, тот крутит не ту ручку.
Сначала измерить, потом выделять
Прежде чем менять хотя бы одно значение, нужны две цифры: реально доступная память и текущее потребление.
free -h
cat /proc/meminfo | grep -E 'MemTotal|MemAvailable|SwapTotal'
Решающее значение имеет MemAvailable, а не free. Linux использует незанятый RAM под файловый кэш, поэтому «free» почти всегда мал и почти всегда не имеет отношения к делу.
Реальное потребление сервера видно так:
ps -o pid,rss,cmd -C java
Значение RSS указано в килобайтах, это память, которую процесс занимает в RAM. Оно всегда больше, чем -Xmx, и именно на этом месте большинство инструкций заканчивается.
Почему «как можно больше» гарантированно оборачивается проблемой
Кроме heap, JVM нужен целый ряд других областей памяти, которые -Xmx вообще не учитывает:
- Metaspace: загруженные классы. У модпака из 300 модов это легко 300-600 МБ.
- Thread-стеки: каждый поток получает около 1 МБ. Chunk-worker, Netty-потоки, планировщики плагинов, в сумме набегает 100-300 МБ.
- Direct Buffers: весь сетевой трафик Netty обрабатывает в памяти за пределами heap. При большом числе одновременных игроков несколько сотен мегабайт совершенно нормальны.
- Code-cache и структуры GC: JIT-компилятор и собственный учёт G1 стоят примерно 5-10% от размера heap.
Надёжное практическое правило звучит так: считайте Xmx плюс 1-1,5 ГБ на саму JVM плюс минимум 512 МБ на операционную систему. Для модпака с большим количеством модов лучше Xmx плюс 2 ГБ.
На сервере с 8 ГБ RAM это означает -Xmx6G, а не -Xmx8G. Кто выделит 8, получит уже не ошибку heap, а кое-что похуже: процесс, который ядро без предупреждения убивает прямо посреди сохранения мира. Ошибка heap — это аккуратный, задокументированный отказ. OOM-kill может оставить после себя повреждённые region-файлы.
Есть и вторая причина не выделять максимум: слишком большой heap замедляет сборку мусора. G1 приходится просматривать больше памяти, паузы при mixed GC становятся длиннее, и из редкого подлагивания вырастает заметный freeze. Выше примерно 12 ГБ соотношение в Minecraft, как правило, уходит в минус. Кому нужно больше, тому стоит разделить мир, а не увеличивать heap.
Ориентиры по числу игроков и модпаку
Эти значения — отправная точка, а не закон природы. Они рассчитаны на мир обычного размера и включённую предгенерацию.
| Тип сервера | Игроки | Xmx | RAM в системе |
|---|---|---|---|
| Vanilla или Paper, без плагинов | до 10 | 2G | 4 ГБ |
| Paper с 15-30 плагинами | 10-30 | 4G | 8 ГБ |
| Paper, большой набор плагинов, база данных | 30-80 | 6G-8G | 12-16 ГБ |
| Лёгкий модпак, до 120 модов | до 10 | 6G | 8 ГБ |
| Средний модпак, 150-250 модов | до 20 | 8G-10G | 16 ГБ |
| Тяжёлый модпак, от 300 модов | до 20 | 10G-12G | 16-24 ГБ |
| Proxy (Velocity, BungeeCord) | любое | 512M-1G | 2 ГБ |
Два замечания к таблице. Во-первых, у модпаков потребность в heap растёт почти исключительно с числом модов и размером мира, а не с числом игроков. Во-вторых, самый действенный рычаг вообще: параметр view-distance в файле server.properties. Снижение с 10 до 8 часто экономит больше памяти, чем 2 ГБ дополнительного heap, потому что количество загруженных чанков растёт квадратично относительно дальности прорисовки. Установка simulation-distance в 6 дополнительно снижает нагрузку на CPU.
Xms равен Xmx: прогрев
-Xms определяет, с каким объёмом heap стартует JVM. Если там стоит значение меньше -Xmx, heap во время работы растёт постепенно. Каждое увеличение означает полную сборку мусора и свежие page faults на стороне операционной системы, а так как G1 умеет и уменьшать heap обратно, вся история повторяется. Именно отсюда берутся печально известные подлагивания каждые несколько минут в первые часы после старта.
Всегда задавайте -Xms равным -Xmx. Вместе с -XX:+AlwaysPreTouch JVM при старте один раз затрагивает каждую отдельную страницу памяти heap. Старт из-за этого длится на 2-15 секунд дольше, в зависимости от размера heap, зато во время игры page faults больше не возникают. Приятный побочный эффект: если -Xmx выбран слишком большим, это, как правило, выясняется уже при старте, а не тремя часами позже посреди игры.
Но не полагайтесь при этом на короткую пробную команду с -version. Проверено на практике: java -Xms4G -Xmx4G -XX:+AlwaysPreTouch -version без нареканий отработала в окружении, ограниченном 2 ГБ, потому что вызов завершается раньше, чем heap реально начинает использоваться. Настоящее доказательство даёт только полноценный запуск сервера с последующим наблюдением за ps -o rss= -C java.
Проверенная команда запуска для Paper выглядит так (так называемые 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
Начиная с 12 ГБ heap PaperMC рекомендует изменённые значения: G1NewSizePercent=40, G1MaxNewSizePercent=50, G1HeapRegionSize=16M, G1ReservePercent=15 и InitiatingHeapOccupancyPercent=20.
Настоящая польза заключена в двух последних строках, и почти в каждой инструкции их не хватает. -XX:+HeapDumpOnOutOfMemoryError при аварии записывает полный образ памяти, по которому потом можно доказать причину. -XX:+ExitOnOutOfMemoryError немедленно завершает JVM вместо того, чтобы оставлять её работать в полумёртвом состоянии, когда игроки подключаются и теряют прогресс. Вместе с Restart=on-failure в unit-файле получается корректный перезапуск, смотрите об этом автоматический запуск Minecraft-сервера и создание systemd-сервиса.
Учтите ещё одно: каталог для дампов должен существовать и располагать как минимум таким же объёмом, какой задан в -Xmx. Heap на 8 ГБ создаёт файл .hprof размером 8 ГБ. Если после этого накопитель окажется заполнен, вы получите вторую проблему, смотрите переполненный диск в Linux.
Версия Java и различия между системами
От того, какая JVM используется, поведение меняется заметно:
- Java 8 по умолчанию использует Parallel-коллектор, а не G1. Здесь встречается и вариант
java.lang.OutOfMemoryError: GC overhead limit exceeded, который означает, что более 98% времени было потрачено на сборку мусора. Логирование GC включается через-XX:+PrintGCDetails -Xloggc:gc.log. - Начиная с Java 9 G1 является стандартом на всех машинах минимум с двумя ядрами и 1792 МБ RAM. Логирование идёт через новый Unified Logging:
-Xlog:gc*:file=logs/gc.log:time,uptime:filecount=5,filesize=10M. Старые флаги здесь отклоняются, и сервер не стартует. - Наличие пакетов: Debian 13 поставляет
openjdk-21-jre-headlessиopenjdk-25-jre-headless, но не Java 17. Debian 12 поставляет Java 17, но ни 21, ни 25. В Ubuntu 22.04 и 24.04 есть 8, 11, 17, 21 и 25. Кому нужна конкретная версия, которой дистрибутив не знает, тот подключает репозиторий Adoptium (Temurin с 8 по 26 для trixie, bookworm, noble и jammy). Подробности в статьях установка Java 21 на Debian и установка Java 17 на Debian.
Ловушка, в которую попадают почти все: диагностических утилит jcmd, jmap, jstat и jstack в пакетах jre-headless нет. Они лежат в openjdk-XX-jdk-headless. Кто держит сервер на JRE, тот в критический момент остаётся без инструментов:
apt install -y openjdk-21-jdk-headless
dnf install -y java-21-openjdk-devel
Первая строка относится к Debian и Ubuntu, вторая к AlmaLinux, Rocky Linux и Oracle Linux. После этого jcmd и jmap находятся в /usr/bin/. Проверяйте это командой command -v jcmd, а не which jcmd: в минимальной установке семейства Red Hat пакета which нет, а в EL 10 он и без того объявлен устаревшим.
Как распознать утечку памяти из-за плагинов
Утечка памяти выглядит иначе, чем простая нехватка памяти. Разница в динамике: при слишком маленьком heap потребление после старта выходит на высокий уровень и там остаётся. При утечке базовый уровень продолжает расти после каждой полной сборки мусора. Именно этот базовый уровень и является ключевым показателем.
Его можно запросить напрямую. Сначала определить ID процесса, затем принудительно вызвать полную сборку и посмотреть на heap:
pgrep -f paper.jar
jmap -histo:live PID | head -30
jcmd PID GC.heap_info
Важное замечание: jmap -histo:live внутри себя запускает полную сборку и работает даже тогда, когда, как выше, задан -XX:+DisableExplicitGC. Напрашивающаяся команда jcmd PID GC.run в такой конфигурации не делает ничего, потому что идёт через System.gc(), а он как раз и отключён. По опыту это стоит получаса недоумения.
Записывайте значение сразу после старта, затем через два, шесть и двенадцать часов. Если после полной сборки значение стабильно растёт, хотя игроков онлайн не прибавилось, это утечка. Список классов из гистограммы обычно уже показывает направление: массово ItemStack, CraftPlayer давно вышедших игроков или объект с именем пакета какого-нибудь плагина.
Заметно удобнее это делается с плагином spark, который доступен для Paper, Fabric и Forge:
/spark healthreport --memoryпоказывает загрузку heap, поведение GC и области вне heap одним взглядом./spark heapsummaryстроит рейтинг классов по потреблению памяти, не записывая при этом дамп размером в несколько гигабайт./spark gcпоказывает частоту и длительность сборок.
Если подозрение падает на конкретный плагин, опровергнуть его просто: удалить плагин, дать серверу поработать 24 часа, снова измерить базовый уровень. Классические кандидаты: плагины, которые кэшируют данные игроков в map-структурах и не очищают их при выходе, а также всё, что связано с редактированием мира и ведёт неограниченную историю отмены.
Swap: спасательный круг, а не расширение памяти
Swap и Java-heap — естественные враги. Сборка мусора регулярно затрагивает большие части heap. Если хотя бы малая их доля лежит на накопителе, пауза в 50 миллисекунд превращается в 20 секунд, и сервер считается зависшим. Никогда не закладывайте swap в размер heap.
Тем не менее swap должен быть, только небольшой и неохотный. Он работает как буфер, чтобы кратковременные пики не запускали сразу OOM-killer, и принимает редко используемые страницы других служб. Рекомендуются 2-4 ГБ и низкая склонность к вытеснению:
swapon --show
cat /proc/sys/vm/swappiness
sysctl -w vm.swappiness=10
На постоянной основе это задаётся в /etc/sysctl.d/99-swappiness.conf. Настройка в деталях описана в статье настройка swap и предотвращение out of memory.
Отдельный случай заслуживает внимания: -XX:+AlwaysPreTouch в сочетании с нехваткой RAM. Так как при старте затрагивается каждая страница heap, излишек сразу уходит в swap. Сервер при этом стартует, но с первого же тика работает недопустимо медленно. Если сервер после включения PreTouch внезапно стартует крайне вяло, виноват слишком большой -Xmx, а не PreTouch.
В качестве жёсткого верхнего предела дополнительно можно задать MemoryMax в systemd-unit, например MemoryMax=7G при 8 ГБ RAM. Тогда при выходе ситуации из-под контроля ядро прицельно убьёт процесс Minecraft, а не базу данных или доступ по SSH.
Как понять, что проблема действительно устранена
Перезапуск без немедленного падения ничего не доказывает. Проверьте вместо этого пять пунктов после 24 часов работы под обычной нагрузкой:
jcmd PID GC.heap_info: занятый heap сразу после полной сборки должен быть заметно ниже 70% от-Xmxи оставаться стабильным со временем.ps -o rss= -C java: значение должно установиться примерно на уровнеXmxплюс 1-1,5 ГБ и дальше не расти. Если оно растёт, а heap остаётся стабильным, утечка находится за пределами heap, обычно в Metaspace или в Direct Buffers.free -h: значение available не должно опускаться ниже примерно 500 МБ.swapon --show: объём занятого swap должен оставаться близким к нулю.- Лог GC: полные сборки («Pause Full») практически не должны встречаться, обычные паузы должны оставаться ниже 200 миллисекунд. Серия Full GC одна за другой — верный предвестник следующего
OutOfMemoryError, часто он наступает через четверть часа.
Дополнительно команда /tps или /spark tps в игре показывает, держится ли tickrate стабильно на 20,0. Сервер, здоровый с точки зрения памяти, удерживает это значение и спустя часы работы.
Если Java вообще не запускается
Четыре сообщения дословно, которые типично появляются при изменении параметров памяти:
Invalid maximum heap size: -Xmx8GB— самая частая опечатка. Единица называетсяG, а неGB. Допустимыk,mиg, в верхнем или нижнем регистре.Initial heap size set to a larger value than the maximum heap sizeозначает, что-Xmsбольше, чем-Xmx, чаще всего потому, что при копировании поправили только одно из двух значений.Could not reserve enough space for object heapозначает, что выделение превышает доступную память. На 32-битной JVM предел в любом случае наступает чуть ниже 4 ГБ, независимо от установленного объёма RAM. Проверяется командойjava -version, там должно стоять64-Bit Server VM.Unrecognized VM option 'UseG1GC'или похожее указывает на слишком старую или неподходящую JVM. Некоторым экспериментальным флагам обязательно нужен предшествующий-XX:+UnlockExperimentalVMOptions, причём перед соответствующим флагом в командной строке.
С какими значениями JVM в итоге действительно работает, можно перепроверить в любой момент. Без запущенного сервера, по значениям по умолчанию:
java -XX:+PrintFlagsFinal -version 2>/dev/null | grep -w MaxHeapSize
Две мелочи здесь сделаны намеренно. 2>/dev/null поглощает баннер версии, который JVM пишет в поток ошибок и который иначе нефильтрованным проходит мимо grep прямо в вывод. А grep -w MaxHeapSize вместо grep -i maxheapsize действительно возвращает всего одну строку: нечёткий поиск находит дополнительно SoftMaxHeapSize, и тот, кто ждёт одно значение, легко считывает не то.
А на работающем процессе, по реально активным флагам:
jcmd PID VM.flags
Это самый надёжный способ вскрыть удивительно распространённую ситуацию, когда скрипт запуска изменили, а сервер по-прежнему работает со старыми значениями из второго файла скрипта.
Коротко: сначала измерьте, действительно ли проблема в heap. Затем выделите столько, сколько нужно миру, и оставьте минимум 1,5 ГБ на JVM и систему. Задайте -Xms равным -Xmx, включите heap-дамп на случай аварии и понаблюдайте за базовым уровнем после полной сборки в течение нескольких часов. Разница между «работает» и «работает стабильно» заключена именно в этом последнем шаге.
Базовая настройка самого сервера описана в статьях установка Minecraft-сервера на Debian и чек-лист для нового root-сервера.
Частые вопросы
Сколько RAM выделять Minecraft-серверу?
Почему Xms должен быть равен Xmx?
В чём разница между OutOfMemoryError и процессом, который просто исчезает с сообщением Killed?
Как распознать утечку памяти из-за плагина?
Помогает ли больший swap против ошибки Java heap space?
Почему я не нахожу jcmd и jmap на своём сервере?
2026 KernelHost GmbH. Все права защищены. Эта инструкция охраняется авторским правом. Публикация на других сайтах, в том числе частично или в изменённом виде, без нашего письменного согласия не разрешается. Цитирование с указанием источника и активной ссылкой мы приветствуем.

