Программирование JavaJVM и памятьJava-разработчик серверных приложений

Heap остаётся свободным, но приложение получает OutOfMemoryError: Direct buffer memory. Какой механизм объя...

Heap остаётся свободным, но приложение получает OutOfMemoryError: Direct buffer memory. Какой механизм объясняет это расхождение?

Проходите собеседования с ИИ помощником Hintsage

Краткий ответ

Исчерпана не Java-память heap, а ограниченная область нативной памяти, используемая прямыми буферами. Объект ByteBuffer находится в heap, но его содержимое размещается вне heap, поэтому свободный heap не гарантирует возможность создать ещё один прямой буфер.

Исторический контекст

Прямые буферы появились для эффективного взаимодействия Java с операционной системой и сетевыми каналами. Данные можно передавать между JVM и нативным кодом с меньшим числом копирований, чем при использовании обычных heap-буферов.

Цена этого подхода — памятью буфера управляет не только сборщик мусора. JVM должна учитывать отдельный лимит прямой памяти, а освобождение нативной области обычно связано с обнаружением и очисткой соответствующего Java-объекта.

Постановка проблемы

Приложение может иметь небольшой используемый heap, но одновременно удерживать много крупных прямых буферов. Это приводит к OutOfMemoryError: Direct buffer memory, хотя обычный heap dump не выглядит критически заполненным.

Неверный вывод — просто увеличить heap. Он не устраняет причину, если лимит прямой памяти не изменён, а сами буферы продолжают создаваться или долго удерживаться.

Подробное решение

При создании прямого буфера JVM резервирует нативную память и создаёт Java-объект-обёртку в heap. Внутри обёртки хранится адрес внешней области памяти, но сами байты не являются частью Java heap.

Размер доступной прямой памяти ограничивается параметром JVM для direct memory. При попытке очередного резервирования JVM проверяет этот лимит; если памяти недостаточно, она может инициировать сборку мусора в надежде найти недостижимые буферы. Если освобождения недостаточно, выбрасывается OutOfMemoryError.

Сборка мусора не обязана немедленно освобождать нативную память после того, как логическая работа с буфером завершена. Пока объект-обёртка достижим — например, через очередь, кэш, незакрытый запрос или зависший future, — связанная область сохраняется.

Для диагностики нужно сопоставить несколько источников:

  • метрики пула буферов через BufferPoolMXBean для direct и mapped buffers;
  • heap dump, чтобы найти удерживающие ссылки на объекты-обёртки;
  • JFR или профилировщик для анализа частоты и размеров аллокаций;
  • RSS процесса и инструменты нативной памяти, чтобы отличить heap от внешних расходов;
  • настройки лимита direct memory и фактический жизненный цикл буферов.

Увеличение лимита может быть оправдано, если объём буферов рассчитан и процесс имеет достаточный запас нативной памяти. Но это не лечение утечки: чрезмерный лимит способен привести к росту RSS, давлению на память операционной системы и завершению процесса уже за пределами контролируемого heap.

Предпочтительное решение — ограничить число и суммарный размер буферов, корректно освобождать связанные ресурсы, настроить backpressure и не удерживать буферы дольше необходимого. Пул буферов уменьшает стоимость аллокаций, но требует строгого контроля владения: повторное использование ещё занятого буфера создаёт ошибки данных.

Ситуация из практики

Сетевой сервис обрабатывает крупные сообщения и передаёт их через прямые буферы. Heap-метрики стабильны, но после всплеска нагрузки direct memory постепенно достигает лимита, потому что завершённые операции складывают буферы в очередь повторной обработки.

Рассматривались два варианта. Увеличение лимита direct memory было быстрым, но лишь отодвигало сбой и повышало риск нехватки памяти на уровне процесса. Полный переход на heap-буферы упростил бы управление памятью, но добавил бы копирование при сетевом вводе-выводе.

Выбрали ограниченный пул прямых буферов с явным возвратом после завершения операции, ограничением очереди и метриками count, totalCapacity и memoryUsed. При заполнении пул не создаёт бесконечные новые буферы, а применяет backpressure. В результате потребление direct memory стало ограниченным, а всплески нагрузки перестали приводить к неконтролируемому росту внешней памяти.

Что кандидаты часто упускают

1. Освобождается ли прямой буфер сразу после выхода из метода?

Нет. Выход из метода удаляет только локальную ссылку. Если других ссылок нет, объект становится кандидатом на сборку мусора, но момент сборки не определён. Нативная память, связанная с объектом, обычно освобождается механизмом очистки, запускаемым после обнаружения недостижимости, поэтому полагаться на немедленное освобождение нельзя.

2. Почему heap dump может не показать размер проблемы?

Heap dump описывает объекты в Java heap, а не полный объём памяти, выделенный по адресам, которые хранятся внутри этих объектов. В нём можно увидеть множество небольших обёрток ByteBuffer и цепочки удерживающих ссылок, но фактический размер внешних областей нужно сопоставлять с метриками пулов буферов и инструментами диагностики нативной памяти.

3. Достаточно ли увеличить лимит direct memory, если ошибка исчезла?

Нет, это доказывает только то, что новый лимит пока не достигнут. Если буферы удерживаются дольше ожидаемого или их суммарный объём растёт вместе с нагрузкой, сбой повторится позже. Увеличение лимита безопасно лишь при расчёте максимального числа одновременных буферов, учёте heap, потоков, библиотек и прочей нативной памяти, а также при наличии мониторинга и ограничений нагрузки.