Вход на сайт

Просмотр новости

Найдите то, что Вас интересует

[Перевод] JDK 28 ускорит работу с нативной памятью

Дата публикации: 23-10-2026 13:28:52

Начиная с JDK 28, API Foreign Function and Memory (FFM) сможет выделять небольшие объёмы памяти из повторно используемых пулов нативной памяти, если приложение использует Arena.ofConfined().Главное преимущество этого нововведения — отсутствие необходимости изменять исходный код при существенном сокращении накладных расходов в распространённых сценариях использования FFM.Разбираемся в новом переводе от команды Spring АйО Читать далее

Основное содержимое страницы с новостью.

Уровень сложностиПростой

Время на прочтение7 мин

Охват и читатели3.4K

Обзор

Перевод

Начиная с JDK 28, API Foreign Function and Memory (FFM) сможет выделять небольшие объёмы памяти из повторно используемых пулов нативной памяти, если приложение использует Arena.ofConfined().

Главное преимущество этого нововведения — отсутствие необходимости изменять исходный код при существенном сокращении накладных расходов в распространённых сценариях использования FFM.

Выделение небольших объёмов нативной памяти — распространённая операция

Арены, привязанные к одному потоку (confined arenas), часто используются для временного хранения данных при вызовах нативных функций. Типичный пример:

try (Arena arena = Arena.ofConfined()) {
    MemorySegment result = arena.allocate(ValueLayout.JAVA_INT);
    nativeFunction.invokeExact(result); // Writes a result in the provided segment
    return result.get(ValueLayout.JAVA_INT, 0);
}

Этот подход обеспечивает детерминированное управление временем жизни памяти и строгие гарантии её доступности только одному потоку. После закрытия арены связанные с ней сегменты памяти немедленно становятся недоступными. В частности, такой шаблон часто применяется при обработке errno в системных вызовах.

До JDK 28 даже для выделения крошечного участка памяти мог потребоваться вызов стандартного нативного аллокатора и регистрация соответствующей операции очистки. Если нативной функции нужен лишь указатель, значение типа long или int либо короткая строка, накладные расходы на управление памятью могут оказаться выше затрат на непосредственный доступ к выделенному участку.

Анализ исходного кода и поведения среды выполнения, проведённый в ходе разработки этой оптимизации, подтвердил распространённость подобных сценариев. В одном из запусков тестового набора с инструментированием часть изолированных арен вообще не выделяла нативную память, а более 99,99% арен, выполнявших выделение, использовали менее 64 байт. Хотя нагрузку тестового набора нельзя считать репрезентативной для всех приложений, полученные результаты убедительно свидетельствовали в пользу небольшого пула с ленивой инициализацией.

Как работает пулинг

Каждый платформенный поток может лениво создавать и поддерживать небольшой кеш пулов нативной памяти. По умолчанию такой кеш способен хранить четыре пула по 64 байта каждый.

При создании арена не получает пул автоматически. Его получение откладывается до первого выделения памяти, для которого пулинг может оказаться полезным. Это важно, поскольку арены, не выделяющие память, не приводят к созданию нативных пулов.

После получения пул закрепляется исключительно за соответствующей ареной. Подходящие участки памяти последовательно выделяются из этого пула. Схематично процесс выглядит так:

64-byte pool

0          8         16    20                          63
+----------+----------+-----+-------------------- ... --+
| pointer  |   long   | int | remaining capacity  ...   |
+----------+----------+-----+-------------------- ... --+

Рисунок 1. Выделение небольших участков памяти из пула.

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

При закрытии арены использованная часть её пула очищается, после чего пул возвращается в кеш потока. Если кеш уже заполнен, пул освобождается через нативный аллокатор.

Каждая арена, получившая пул, владеет им эксклюзивно. Поэтому вложенные изолированные арены — например, когда внутри блока try-with-resources одной арены создаётся ещё один блок try-with-resources со второй ареной — остаются независимыми друг от друга.

Если свободного пула в кеше нет, арена пытается выделить собственный локальный пул. При закрытии арены этот пул либо добавляется в кеш, либо освобождается.

Виртуальные потоки

Создание отдельного кеша нативной памяти для каждого виртуального потока свело бы на нет одно из их главных преимуществ — минимальное потребление ресурсов. Поэтому виртуальный поток временно получает пул из кеша платформенного потока-носителя (carrier thread), на котором он в данный момент выполняется.

Виртуальный поток закрепляется за потоком-носителем (pinned) лишь на короткое время передачи пула. Затем пул отсоединяется от кеша потока-носителя и переходит в исключительное владение арены.

Благодаря этому виртуальный поток может безопасно мигрировать между потоками-носителями, пока арена остаётся открытой. После закрытия арены пул возвращается в кеш того потока-носителя, на котором в этот момент выполняется виртуальный поток. Это может быть уже другой поток-носитель.

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

Сохранение гарантий безопасности

Пулинг не меняет правила времени жизни и доступности памяти, установленные API Arena.

После закрытия арены по-прежнему выполняются следующие условия:

  • Область действия арены больше не активна.

  • Доступ к её сегментам памяти невозможен.

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

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

Стандартные операции очистки выполняются до обнуления и освобождения пула. Такой порядок важен, поскольку операция очистки может на законных основаниях обращаться к нативной области памяти через сегмент, используемый для очистки.

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

Изменения в наблюдаемом поведении

Хотя время жизни сегментов памяти не меняется, время существования лежащих в их основе нативных выделений может измениться.

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

Поэтому приложения и диагностические инструменты не должны рассчитывать на однозначное соответствие между выделениями памяти ареной и нативными вызовами malloc или free.

Производительность

Микробенчмарки, проведённые в ходе разработки, показали значительное сокращение задержек при выделении небольших участков памяти в изолированных аренах.

В следующей таблице представлено ускорение при выделении 5 и 20 байт:

Платформа

Выделение 5 байт

Выделение 20 байт

Linux AArch64

12,3×

8,6×

Linux x64

6,8×

7,8×

macOS AArch64

18,6×

16,8×

Windows x64

16,8×

18,9×

Таблица 1. Ускорение на протестированных платформах.

Каждое значение представляет собой отношение среднего времени выполнения операции в JMH без пулинга к времени выполнения с пулингом. Чем выше показатель, тем лучше. Конфигурация бенчмарков и исходные результаты доступны в запросе на включение изменений OpenJDK PR #31365.

Отдельный запуск бенчмарков на компьютере Apple M4 под управлением macOS дал следующие результаты:

Benchmark                                   (size)  Mode  Cnt    Score   Error  Units
AllocTest.alloc_confined                         5  avgt   30    1.052 ± 0.027  ns/op
AllocTest.alloc_confined                        20  avgt   30    1.138 ± 0.023  ns/op
AllocTest.alloc_confined                       100  avgt   30   19.044 ± 0.322  ns/op
AllocTest.alloc_confined                       500  avgt   30   26.383 ± 0.714  ns/op
AllocTest.alloc_confined                      2000  avgt   30   31.292 ± 0.253  ns/op
AllocTest.alloc_confined                      8000  avgt   30   81.119 ± 0.957  ns/op
AllocTest.alloc_confined_no_pool                 5  avgt   30   15.862 ± 0.226  ns/op
AllocTest.alloc_confined_no_pool                20  avgt   30   18.139 ± 0.262  ns/op
AllocTest.alloc_confined_no_pool               100  avgt   30   18.856 ± 0.281  ns/op
AllocTest.alloc_confined_no_pool               500  avgt   30   26.168 ± 0.232  ns/op
AllocTest.alloc_confined_no_pool              2000  avgt   30   31.209 ± 0.314  ns/op
AllocTest.alloc_confined_no_pool              8000  avgt   30   80.840 ± 0.804  ns/op

Результаты бенчмарков на Mac с процессором M4.

При выделении участков памяти, превышающих стандартный размер пула в 64 байта, производительность в целом оставалась на прежнем уровне, поскольку для таких операций используется стандартный механизм выделения.

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

Benchmark                                   (size)  Mode  Cnt    Score   Error  Units
AllocTest.OfVirtual.alloc_confined               5  avgt   30    3.222 ± 0.087  ns/op
AllocTest.OfVirtual.alloc_confined              20  avgt   30    2.737 ± 0.021  ns/op
AllocTest.OfVirtual.alloc_confined             100  avgt   30   18.868 ± 0.199  ns/op
AllocTest.OfVirtual.alloc_confined             500  avgt   30   26.117 ± 0.240  ns/op
AllocTest.OfVirtual.alloc_confined            2000  avgt   30   31.407 ± 0.508  ns/op
AllocTest.OfVirtual.alloc_confined            8000  avgt   30   80.474 ± 0.676  ns/op
AllocTest.OfVirtual.alloc_confined_no_pool       5  avgt   30   15.672 ± 0.090  ns/op
AllocTest.OfVirtual.alloc_confined_no_pool      20  avgt   30   18.109 ± 0.250  ns/op
AllocTest.OfVirtual.alloc_confined_no_pool     100  avgt   30   18.791 ± 0.163  ns/op
AllocTest.OfVirtual.alloc_confined_no_pool     500  avgt   30   25.947 ± 0.174  ns/op
AllocTest.OfVirtual.alloc_confined_no_pool    2000  avgt   30   31.206 ± 0.440  ns/op
AllocTest.OfVirtual.alloc_confined_no_pool    8000  avgt   30   79.711 ± 0.576  ns/op

Результаты бенчмарков виртуальных потоков на Mac с процессором M4.

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

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

Кто получит преимущества?

Эта оптимизация особенно актуальна для следующих сценариев:

  • Привязки FFM, создающие изолированную арену при каждом вызове нативной функции.

  • Автоматически генерируемые обёртки, подобные создаваемым с помощью jextract.

  • Небольшие значения и структуры языка C.

  • Нативные дескрипторы и выходные параметры, передаваемые через указатели.

  • Короткие строки и массивы байтов, передаваемые в нативный код.

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

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

Прозрачная оптимизация

Наиболее полезные улучшения производительности нередко оказываются теми, ради которых разработчикам не приходится осваивать новый API или переписывать уже работающий код.

Изолированные арены с поддержкой пулинга сохраняют свойства Arena.ofConfined(): детерминированное управление временем жизни памяти, инициализацию нулями и ограничение доступа одним потоком.

Изменения происходят на более низком уровне: теперь при множестве небольших кратковременных выделений памяти можно избежать повторных обращений к нативному аллокатору.

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

Дополнительные материалы

Если вы хотите узнать больше о пулинге памяти в изолированных аренах, ознакомьтесь со следующими материалами:

Присоединяйтесь к русскоязычному сообществу разработчиков на Spring Boot в телеграм — Spring АйО, чтобы быть в курсе последних новостей из мира разработки на Spring Boot и всего, что с ним связано.

Схожие новости

#Наименование новостиТональностьИнформативностьДата публикации
1Ваш кэш в Redis неэффективен, что с этим делать?-17.9610-08-2026
2In-memory база врёт: 5 расхождений с продовой БД016.9507-07-2026
3[Перевод] Паттерны доступа к данным, которые выбесят ваш процессор08.5503-07-2026
4Управление памятью в контейнерах STL. От абстракций к реализации07.6923-10-2026
5Конвейеры на Channels: как не получить тихую утечку памяти и вечно висящий воркер08.427-07-2026
6Можно ли доверить ИИ миграцию Spring Boot 2 → 3?017.7223-10-2026
7Пишем логи в journald с помощью Logback и FFM API-17.8910-08-2026
8API быстрее UI — не закон: замеры на 400+ Android UI-тестах013.3923-10-2026
9EmbeddingGemma 2: как перейти с 768 на 512 координат и сэкономить память на RTX 5090016.4223-10-2026

Классификация: Пресс-релизы. Схожих патентов: 0. Схожих новостей: 9. Тональность: 0. Информативность: 10.72. Источник: habr.com.