- Статус
- Оффлайн
- Регистрация
- 20 Фев 2025
- Сообщения
- 86
- Реакции
- 37
Предпосылки
К началу 2010-х годов Java Native Interface (JNI), созданный ещё в 1997 году, накопил множество критических недостатков:
Производительность. Каждый вызов нативной функции через JNI требовал создания JNI-фрейма, перехода между Java и нативным кодом с переключением контекста, маршалинга данных между типами Java и C. Это создавало значительные накладные расходы — вызов простой нативной функции мог быть в 10-100 раз медленнее прямого вызова.
Сложность разработки. Для интеграции с нативной библиотекой требовалось писать промежуточный нативный слой, вручную маршалить все типы данных, управлять локальными и глобальными ссылками на Java-объекты, обрабатывать исключения в обоих направлениях.
Проблемы со сборщиком мусора. Нативный код мог держать ссылки на Java-объекты, что усложняло работу GC. Необходимо было вручную управлять локальными и глобальными ссылками, что часто приводило к ошибкам.
К 2014 году стало ясно, что JNI сдерживает развитие Java в областях, требующих интенсивного взаимодействия с нативными библиотеками — машинное обучение, обработка данных, высокопроизводительные вычисления.
История развития
Project Panama был запущен в 2014 году и прошёл через несколько ключевых этапов:
2014-2019: Исследования и прототипы. Разработка концепции Foreign Function Interface и Vector API. Первые эксперименты показали возможность достижения производительности, близкой к прямым нативным вызовам.
2019-2021: Foreign Memory Access API. Появился API для безопасной работы с off-heap памятью. Прошёл через несколько итераций в статусе инкубатора (JEP 370, 383, 393), постепенно стабилизируясь.
2021-2022: Объединение в FFM API. В Java 17-19 Foreign Memory Access API объединился с Foreign Linker API в единый Foreign Function & Memory API. Переход в статус preview означал приближение к финальной версии.
2023-2024: Финализация. В Java 22 (март 2024) FFM API стал финальной фичей через JEP 454. API стабилизировался и готов к использованию в продакшене без preview флага.
Архитектура Panama FFM
MemorySegment — работа с нативной памятью. Представляет непрерывный участок памяти (on-heap, off-heap или mapped). В отличие от прямого доступа через указатели в JNI, MemorySegment предоставляет безопасный типизированный доступ с проверками границ. Поддерживает как примитивные типы, так и сложные структуры данных.
Arena — управление временем жизни памяти. Контролирует жизненный цикл MemorySegment через явные области видимости. Автоматически освобождает всю связанную память при закрытии арены, предотвращая утечки. Поддерживает несколько режимов: автоматический (с GC), confined (привязка к потоку), shared (многопоточный доступ).
SegmentAllocator — аллокация памяти. Предоставляет различные стратегии выделения памяти: разовая аллокация, arena-based allocation, stack-like allocation. Позволяет эффективно работать с временными данными без накладных расходов на GC.
Linker — вызов нативных функций. Создаёт MethodHandle для нативных функций без написания JNI-кода. Автоматически генерирует оптимизированные адаптеры для маршалинга данных. JIT-компилятор может инлайнить такие вызовы, достигая производительности близкой к прямым нативным вызовам.
FunctionDescriptor — описание сигнатур функций. Декларативно описывает типы параметров и возвращаемого значения нативной функции. Использует ValueLayout для точного указания размеров и выравнивания типов. Поддерживает передачу структур по значению, variadic функции и соглашения о вызовах.
SymbolLookup — поиск символов в библиотеках. Загружает нативные библиотеки и находит экспортируемые функции. Поддерживает поиск в системных библиотеках через Linker.nativeLinker().defaultLookup(). Позволяет работать с символами без явной загрузки библиотек через System.loadLibrary.
Все компоненты работают совместно, обеспечивая безопасность через compile-time и runtime проверки, производительность через оптимизации JIT-компилятора, и удобство через декларативный API.
Примеры использования
Рассмотрим практический пример работы с Panama FFM API.
Вызов стандартной функции strlen из libc:
import java.lang.foreign.Arena;
import java.lang.foreign.FunctionDescriptor;
import java.lang.foreign.Linker;
import java.lang.foreign.ValueLayout;
import java.nio.charset.StandardCharsets;
public class StrlenExample {
public static void main(String[] args) throws Throwable {
final var linker = Linker.nativeLinker();
final var stdlib = linker.defaultLookup();
final var strlenAddr = stdlib.find("strlen").orElseThrow();
final var strlenDescriptor = FunctionDescriptor.of(
ValueLayout.JAVA_LONG, // size_t
ValueLayout.ADDRESS // const char*
);
final var strlen = linker.downcallHandle(
strlenAddr,
strlenDescriptor
);
try (final var arena = Arena.ofConfined()) {
final var str = arena.allocateFrom("Hello World!", StandardCharsets.UTF_8);
final var length = (long) strlen.invoke(str);
System.out.printf("Length: %d", length);
}
}
}
Сравнение с JNI
Производительность
Бенчмарки показывают, что Panama FFM опережает JNI — около 50 наносекунд против 57 наносекунд у JNI в тестах на вызов нативных функций
Пожалуйста, авторизуйтесь для просмотра ссылки.
. В ранних версиях Panama (2019 год) производительность уступала JNI, но после переработки API в JDK 15-19, FFM API превзошёл JNI
Пожалуйста, авторизуйтесь для просмотра ссылки.
.Альтернативные решения показали худшие результаты: JNA — 4037 нс, JNR — 401 нс, BridJ — 1088 нс.
Удобство разработки
JNI требует написания нативного кода, генерации заголовочных файлов, ручного маршалинга данных и компиляции для каждой платформы. Panama FFM работает полностью на Java с декларативным описанием сигнатур и автоматической генерацией биндингов через jextract. Объём работы сократился на 90% по сравнению с JNI
Пожалуйста, авторизуйтесь для просмотра ссылки.
.Заключение
С финализацией FFM API в Java 22 появилась современная замена JNI, превосходящая её по производительности и удобству.
Детальные результаты бенчмарков:
Пожалуйста, авторизуйтесь для просмотра ссылки.
Пожалуйста, авторизуйтесь для просмотра ссылки.