Статьи

Собственное распределение памяти в Java

Время от времени мы сталкиваемся с проблемой памяти, которая связана не с кучей Java,  а с собственной памятью . Давайте представим ситуацию, когда у нас есть работающий контейнер, который перезапускается один раз в день. Мы смотрим на Прометей / Графана, чтобы выяснить причину этой проблемы. Однако мы не обнаруживаем никаких проблем с размером кучи Java (экспортируется через JMX) и начинаем обвинять наш планировщик контейнеров в выполнении каких-либо неудобных операций с нашими контейнерами :). Настоящая проблема может быть скрыта чуть глубже — в Native Memory.

Используете ли вы какой-либо встроенный распределитель памяти в вашем коде, или вы используете какую-либо библиотеку, которая хранит свое внутреннее состояние вне кучи Java? Это может быть любой сторонний код, который пытается минимизировать GC, например, библиотеки ввода-вывода или базы данных / кэши, хранящие данные в памяти внутри вашего процесса.

В этой статье я расскажу о некоторых основах: как распределена встроенная память в этих библиотеках, и, в основном, как заметить, что что-то в вашем коде начало выделять встроенную память, что может привести к уничтожению вашего контейнера.

Как выделить родную память

Долгое время использовался единственный эффективный способ выделения памяти из Java sun.misc.Unsafe. С самого начала архитекторы Java предупреждают, что эта библиотека предназначена только для внутренних сценариев использования, и нет намерения поддерживать ее обратно совместимой в качестве официального API. Однако разработчики хотели получить лучшее для своих библиотек и продолжали использовать Unsafe в своем коде. Большой взрыв пришел с Java 9 и введением в систему модулей Java. Небезопасный  был автоматически помещен в jdk.unsupportedмодуль, и было объявлено, что модуль станет видимым для внешнего кода в зависимости от него в течение некоторого времени. Но намерение состоит в том, чтобы предоставить официальный API, и библиотеки будут вынуждены перейти на него.

Другой способ выделить собственную память — ByteBuffer . Есть две реализации: HeapByteBufferи DirectByteBuffer. Хотя HeapByteBuffer данные хранятся в байтовом массиве, выделенном в куче, они DirectByteBuffer поддерживаются собственной памятью и в основном используются для передачи данных между JVM и ядром. Тем не менее, некоторые библиотеки делают ввода / вывода реализовали свои собственные библиотеки для работы с родной памяти, такие как Нетти  ( ByteBufMaven ) или Aeron  (подпроект Agrona ), особенно из — за двух причин: обычай сбора неиспользованной памяти (Нетти использует реализация ручного сборщика ссылок (подсчет ссылок) и более удобный API.

Вы можете себе представить, что у владельцев библиотеки не было причин мигрировать из небезопасных . Поэтому проект «Панама» , касающийся взаимосвязи JVM и нативного кода , вышел с чем-то новым и блестящим. Первым выходом этого проекта был JEP 370: API доступа к внешней памяти , который предоставил альтернативный способ доступа к собственной памяти, поддерживаемый JIT-компилятором,  чтобы максимально оптимизировать его, чтобы быть ближе к эффективности Unsafe., В дополнение к этому, он также принес новый API для разработки собственной структуры памяти, чтобы избежать «подсчета» адресов и смещений вручную и обеспечить явный способ освобождения памяти в исходном коде (например, с помощью try-with-resources). Он доступен в качестве инкубатора на Java 14. Не стесняйтесь попробовать и дать отзыв!

Идем прямо к примеру

После короткого вступления давайте воспользуемся самым простым способом выделить собственную память и посмотрим, что она на самом деле делает. 


Джава