W3docs

API de Función Foránea y Memoria en Java

Llama a código nativo y accede a memoria fuera del heap en Java moderno con la API de Función Foránea y Memoria.

La API de Función Foránea y Memoria (FFM) es la forma moderna y segura de Java para hacer dos cosas que antes requerían la frágil Interfaz Nativa de Java (JNI): llamar a funciones escritas en C y otros lenguajes nativos, y leer y escribir memoria que vive fuera del heap de Java. Se convirtió en una característica final en JDK 22 y reside en el paquete java.lang.foreign.

Este capítulo cubre cómo funciona la memoria fuera del heap en FFM, cómo un Arena controla su ciclo de vida, cómo los layouts describen datos nativos y cómo llamar a una función de C desde Java. Al final deberías entender cuándo FFM es la herramienta adecuada y cómo encajan sus piezas.

Por Qué FFM Reemplaza a JNI

Antes de FFM, hablar con código nativo implicaba código JNI escrito a mano, buffers de bytes manuales y un riesgo constante de bloquear la JVM con un puntero erróneo. Un solo tipo mal coincidente o un desplazamiento fuera de rango podía corromper el heap o provocar un segfault en todo el proceso — y como el fallo ocurría en código nativo, no obtenías ningún stack trace de Java.

FFM reemplaza todo eso con una API pequeña y con seguridad de tipos construida alrededor de tres ideas:

  • Un Arena controla el ciclo de vida de la memoria: cuando se cierra, todo lo que asignó queda liberado.
  • Un MemorySegment es una vista con comprobación de límites de esa memoria, por lo que el acceso fuera de rango lanza una excepción en lugar de corromper la memoria.
  • Un Linker construye un manejador invocable para una función nativa, mapeando tipos de C a tipos de Java de antemano.

El resultado es que los errores aparecen como excepciones de Java en tiempo de enlace, no como fallos aleatorios más adelante. El resto de este capítulo recorre cada pieza por turno.

Memoria Fuera del Heap con Arena y MemorySegment

Un MemorySegment es una región contigua de memoria con un tamaño conocido. A diferencia de un array de Java, puede vivir fuera del heap, por lo que el recolector de basura nunca lo mueve y puede pasarse directamente a código nativo. Nunca construyes un segmento directamente — se lo pides a un Arena, y el arena posee el ciclo de vida del segmento.

Cuando el arena se cierra, cada segmento que asignó queda liberado de una vez. Esto hace que las fugas y los errores de uso posterior a la liberación sean difíciles de escribir: toca un segmento después de que su arena se cierre y obtendrás una excepción, no un fallo.

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

Cada lectura y escritura pasa por un ValueLayout, que indica exactamente cuántos bytes ocupa un valor y cómo está dispuesto. Eso es lo que mantiene cada acceso con comprobación de límites y con seguridad de tipos.

Elegir un Arena

Arena es el gestor del ciclo de vida, y el método de fábrica que elijas decide quién puede tocar la memoria y cuándo se libera. Elegir el correcto es la principal decisión de seguridad en el código FFM.

ArenaCiclo de vidaAcceso de hilos
Arena.ofConfined()Hasta close()Solo el hilo creador
Arena.ofShared()Hasta close()Cualquier hilo
Arena.ofAuto()Hasta que el GC lo recolectaCualquier hilo
Arena.global()Todo el programaCualquier hilo

Usa ofConfined() para el caso común: memoria de corta duración usada por un hilo y liberada de forma determinista con try-with-resources. Recurre a ofShared() solo cuando varios hilos deben leer el mismo segmento, y a ofAuto() cuando no puedes marcar fácilmente el final del ciclo de vida. Si tu código usa hilos virtuales, prefiere ofShared() o ofAuto(), ya que un arena confinado está ligado a un hilo portador.

Describir Layouts

Un ValueLayout describe un único valor primitivo; un MemoryLayout puede describir structs y arrays completos. Los layouts te permiten calcular desplazamientos y tamaños sin codificar números mágicos, lo que mantiene el acceso a structs nativos legible.

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
}

Los campos con nombre y los accesores PathElement significan que describes el struct una vez y dejas que la API calcule los desplazamientos de bytes por ti.

Llamar a Funciones Nativas con Linker

La característica principal de FFM es el downcall: invocar una función de C desde Java. Obtienes el Linker de la plataforma, buscas la dirección de la función con un SymbolLookup, describes su firma con un FunctionDescriptor y recibes un MethodHandle que puedes invocar como cualquier método de 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
}

El FunctionDescriptor mapea tipos de C a portadores de Java: un puntero de C se convierte en ValueLayout.ADDRESS, un size_t de C se mapea a JAVA_LONG, un int de C a JAVA_INT. Obtén el mapeo correcto y la llamada es con seguridad de tipos; equivócate y lo descubres en tiempo de enlace, no como un fallo aleatorio. Dado que las llamadas nativas escapan de la red de seguridad de la JVM, FFM es una operación restringida — el módulo que la usa debe tener acceso concedido con el indicador --enable-native-access.

Un Ejemplo Completo y Ejecutable

La API java.lang.foreign es una característica de vista previa antes de JDK 22, por lo que el programa siguiente ejecuta las mismas dos ideas — memoria fuera del heap y manejo de strings al estilo nativo — usando solo las clases JDK siempre activas que FFM fue diseñado para reemplazar. Un ByteBuffer directo es memoria asignada fuera del heap de Java, igual que un MemorySegment; leer valores tipados en desplazamientos de bytes refleja un acceso con ValueLayout; y escanear bytes hasta un terminador cero es exactamente lo que hace strlen de C.

java— editable, runs on the server

Lo que hay que observar de la ejecución:

  • isDirect = true confirma que el buffer está asignado fuera del heap de Java — la misma propiedad que permite que un MemorySegment se pase de forma segura a código nativo sin que el GC lo reubique.
  • Escribir (i + 1) * 10 en cada desplazamiento de 4 bytes y leerlo de vuelta produce 10, 20, 30, 40 con sum = 100, lo que muestra que la memoria fuera del heap es almacenamiento real, indexable y tipado igual que un MemorySegment.
  • byteSize = 16 son cuatro enteros de 4 bytes — el direccionamiento por desplazamiento de bytes explícito es exactamente cómo un ValueLayout calcula posiciones en la API FFM real.
  • El cString construido a mano termina en un byte cero, por lo que el escaneo al estilo strlen se detiene ahí: strlen of the C string = 16 coincide con Java String.length() = 16, lo que prueba que el terminador nulo marca el final como espera C.
  • Ningún buffer se libera a mano — los buffers directos se recuperan cuando son inalcanzables, reflejando Arena.ofAuto(), mientras que el arena real ofConfined() de FFM liberaría de forma determinista en close().

Cuándo Usar FFM

FFM es una herramienta especializada, no una de uso cotidiano. Recurre a ella cuando realmente necesites interoperabilidad nativa o memoria fuera del heap:

  • Llamar a una biblioteca nativa existente — un codec de imágenes en C, un controlador de base de datos, un SDK de hardware — sin escribir código JNI.
  • Compartir buffers grandes con código nativo donde copiar al heap de Java sería un desperdicio, como en pipelines de gráficos o audio.
  • Trabajar con conjuntos de datos muy grandes fuera del heap que no deberían presionar al recolector de basura.

Para el trabajo ordinario con archivos y buffers, quédate con APIs de mayor nivel como Java NIO; son más simples y seguras por defecto. Y recuerda que FFM es una operación restringida: dado que las llamadas nativas escapan de las garantías de seguridad de la JVM, debes lanzar con --enable-native-access o recibirás una advertencia o error en tiempo de ejecución.

Práctica

Práctica
En la API FFM, ¿cuál es el papel de un Arena?
En la API FFM, ¿cuál es el papel de un Arena?
Was this page helpful?