Кейс: heap в норме, а под всё равно падал по памяти

Сервис читал файлы через NIO и держал пул DirectByteBuffer для скорости — обычная оптимизация, чтобы не копировать данные между heap и native-памятью при каждом чтении. JVM heap на графиках держался стабильно на 60% от лимита, GC логи были чистые.

Под всё равно уходил в OOMKilled раз в несколько часов. Дело было не в heap: DirectByteBuffer — это маленький Java-объект на heap с указателем на память, выделенную вне heap через malloc. Освобождение native-памяти происходит только когда GC собирает сам объект-обёртку и вызывает Cleaner. Если обёртки жили долго — пул их не выпускал — native-память росла без остановки, а heap-метрики этого вообще не видели.

Решение — явное освобождение через Cleaner API или переход на пул с чётким lifecycle, где буфер возвращается и переиспользуется, а не ждёт, пока GC доберётся до обёртки.

Ловушка: -Xmx ограничивает только heap. Direct-память по умолчанию ограничена размером heap (-XX:MaxDirectMemorySize неявно равен -Xmx), но если её выставили руками больше — под может исчерпать память хоста, а JVM продолжит думать, что всё в порядке.

Метрики heap ничего не говорят о native-памяти — DirectByteBuffer, Netty pooled allocator и NIO живут за пределами того, что видит GC-дашборд.

Тренажёр: 600 вопросов, мок с таймером, план повторов

senior·base — что спрашивают на самом деле


В этом посте были ссылки, но мы их удалили по правилам Сетки