W3docs

Java Foreign Function & Memory API

Вызов нативного кода и доступ к внеcкучной памяти в современном Java с помощью Foreign Function and Memory API.

Foreign Function and Memory (FFM) API — это современный и безопасный способ решить в Java две задачи, которые прежде требовали хрупкого Java Native Interface (JNI): вызывать функции, написанные на C и других нативных языках, а также читать и записывать память, находящуюся за пределами кучи Java. Этот API стал финальной возможностью в JDK 22 и находится в пакете java.lang.foreign.

В этой главе рассматривается, как работает внеcкучная память в FFM, как Arena управляет её временем жизни, как макеты описывают нативные данные и как вызвать функцию C из Java. По её окончании вы поймёте, когда FFM является подходящим инструментом и как его части сочетаются друг с другом.

Почему FFM заменяет JNI

До появления FFM взаимодействие с нативным кодом означало написание вручную склейки JNI, работу с ручными байтовыми буферами и постоянный риск крашнуть JVM неправильным указателем. Единственное несоответствие типов или ошибка на один байт в смещении могли повредить кучу или вызвать segfault всего процесса — а поскольку крах происходил в нативном коде, стек вызовов Java не выводился.

FFM заменяет всё это небольшим, типобезопасным API, построенным вокруг трёх идей:

  • Arena управляет временем жизни памяти: когда она закрывается, всё выделенное ею освобождается.
  • MemorySegment — это представление с проверкой границ в этой памяти, поэтому выход за пределы диапазона выбрасывает исключение вместо повреждения памяти.
  • Linker создаёт вызываемый дескриптор нативной функции, заблаговременно сопоставляя типы C с типами Java.

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

Внеcкучная память с Arena и MemorySegment

MemorySegment — это непрерывная область памяти известного размера. В отличие от массива Java, она может находиться за пределами кучи, поэтому сборщик мусора никогда не перемещает её, и её можно напрямую передавать нативному коду. Сегмент никогда не создаётся напрямую — вы запрашиваете его у Arena, и arena владеет временем жизни сегмента.

Когда arena закрывается, все выделенные ею сегменты сразу освобождаются. Это делает утечки памяти и ошибки использования после освобождения трудновоспроизводимыми: обратитесь к сегменту после закрытия его arena — и вы получите исключение, а не крах.

import java.lang.foreign.*;

try (Arena arena = Arena.ofConfined()) {
    // Allocate room for four ints, off the Java heap.
    MemorySegment seg = arena.allocate(ValueLayout.JAVA_INT, 4);
    seg.setAtIndex(ValueLayout.JAVA_INT, 0, 100);
    int first = seg.getAtIndex(ValueLayout.JAVA_INT, 0);
    System.out.println(first); // 100
} // arena.close() frees the segment here

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

Выбор Arena

Arena — это менеджер времени жизни, и выбранный фабричный метод определяет, кто может обращаться к памяти и когда она освобождается. Выбор правильного варианта — главное решение по безопасности в коде FFM.

ArenaВремя жизниДоступ из потоков
Arena.ofConfined()До вызова close()Только создавший поток
Arena.ofShared()До вызова close()Любой поток
Arena.ofAuto()До сборки GCЛюбой поток
Arena.global()Всё время работы программыЛюбой поток

Используйте ofConfined() в общем случае: кратковременная память, используемая одним потоком и детерминированно освобождаемая через try-with-resources. Обращайтесь к ofShared() только когда нескольким потокам нужно читать один сегмент, а к ofAuto() — когда конец времени жизни трудно обозначить. Если ваш код использует виртуальные потоки, предпочтите ofShared() или ofAuto(), поскольку confined arena привязана к одному потоку-носителю.

Описание макетов

ValueLayout описывает одно примитивное значение; MemoryLayout может описывать целые структуры и массивы. Макеты позволяют вычислять смещения и размеры, не захардкоживая магические числа, что делает доступ к нативным структурам читаемым.

import java.lang.foreign.*;
import static java.lang.foreign.ValueLayout.*;

// A C struct:  struct Point { int x; int y; };
MemoryLayout point = MemoryLayout.structLayout(
    JAVA_INT.withName("x"),
    JAVA_INT.withName("y")
);

try (Arena arena = Arena.ofConfined()) {
    MemorySegment p = arena.allocate(point);
    var xHandle = point.varHandle(MemoryLayout.PathElement.groupElement("x"));
    var yHandle = point.varHandle(MemoryLayout.PathElement.groupElement("y"));
    xHandle.set(p, 0L, 3);
    yHandle.set(p, 0L, 4);
    System.out.println(xHandle.get(p, 0L) + ", " + yHandle.get(p, 0L)); // 3, 4
}

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

Вызов нативных функций с помощью Linker

Главная возможность FFM — это downcall: вызов функции C из Java. Вы получаете платформенный Linker, ищете адрес функции с помощью SymbolLookup, описываете её сигнатуру с помощью FunctionDescriptor и получаете MethodHandle, который можно вызывать как любой метод Java.

import java.lang.foreign.*;
import java.lang.invoke.MethodHandle;

Linker linker = Linker.nativeLinker();
// strlen lives in the standard C library, found via the default lookup.
MethodHandle strlen = linker.downcallHandle(
    linker.defaultLookup().find("strlen").orElseThrow(),
    // size_t strlen(const char *s);
    FunctionDescriptor.of(ValueLayout.JAVA_LONG, ValueLayout.ADDRESS)
);

try (Arena arena = Arena.ofConfined()) {
    MemorySegment cString = arena.allocateUtf8String("hello");
    long len = (long) strlen.invoke(cString); // 5
}

FunctionDescriptor сопоставляет типы C с носителями Java: указатель C становится ValueLayout.ADDRESS, C size_t отображается на JAVA_LONG, C int — на JAVA_INT. Сопоставьте правильно — и вызов типобезопасен; ошибитесь — и вы узнаете об этом во время компоновки, а не как случайный крах. Поскольку нативные вызовы выходят за пределы сети безопасности JVM, FFM является ограниченной операцией — модуль, использующий её, должен получить доступ с флагом --enable-native-access.

Полный выполняемый пример

API java.lang.foreign являлся предварительной версией до JDK 22, поэтому приведённая ниже программа демонстрирует те же две идеи — внеcкучная память и работа со строками в нативном стиле — используя только всегда доступные классы JDK, которые FFM был призван заменить. Прямой ByteBuffer — это память, выделенная за пределами кучи Java, точно так же как MemorySegment; чтение типизированных значений по байтовым смещениям отражает доступ через ValueLayout; а сканирование байт до нулевого терминатора — это именно то, что делает strlen в C.

java— editable, runs on the server

Что следует вынести из запуска:

  • isDirect = true подтверждает, что буфер выделен за пределами кучи Java — то же свойство, которое позволяет MemorySegment безопасно передаваться нативному коду без перемещения GC.
  • Запись (i + 1) * 10 по каждому 4-байтовому смещению и обратное чтение даёт 10, 20, 30, 40 с sum = 100, показывая, что внеcкучная память — это настоящее, индексируемое, типизированное хранилище, как и MemorySegment.
  • byteSize = 16 — это четыре 4-байтовых int'а; адресация по явному байтовому смещению — это именно то, как ValueLayout вычисляет позиции в реальном FFM API.
  • Вручную созданная cString заканчивается нулевым байтом, поэтому сканирование в стиле strlen останавливается там: strlen of the C string = 16 совпадает с Java String.length() = 16, доказывая, что нулевой терминатор отмечает конец так, как ожидает C.
  • Ни один буфер не освобождается вручную — прямые буферы освобождаются, когда становятся недостижимыми, что соответствует Arena.ofAuto(), тогда как реальная ofConfined() arena FFM освобождала бы детерминированно при close().

Когда использовать FFM

FFM — специализированный инструмент, а не повседневный. Обращайтесь к нему, когда действительно нужна нативная совместимость или внеcкучная память:

  • Вызов существующей нативной библиотеки — кодека изображений на C, драйвера базы данных, SDK оборудования — без написания склейки JNI.
  • Совместное использование больших буферов с нативным кодом, когда копирование в кучу Java расточительно, например, для конвейеров графики или аудио.
  • Работа с очень большими внеcкучными наборами данных, которые не должны нагружать сборщик мусора.

Для обычной работы с файлами и буферами оставайтесь с высокоуровневыми API вроде Java NIO — они проще и безопасны по умолчанию. И помните, что FFM — это ограниченная операция: поскольку нативные вызовы выходят за пределы гарантий безопасности JVM, необходимо запускать программу с --enable-native-access, иначе вы получите предупреждение или ошибку во время выполнения.

Практика

Практика
В FFM API какова роль Arena?
В FFM API какова роль Arena?
Was this page helpful?