W3docs

API de Funções Externas e Memória do Java

Chame código nativo e acesse memória fora do heap no Java moderno com a API de Funções Externas e Memória.

A API de Funções Externas e Memória (FFM) é a forma moderna e segura do Java para fazer duas coisas que antes exigiam a frágil Interface Nativa do Java (JNI): chamar funções escritas em C e outras linguagens nativas, e ler e escrever memória que vive fora do heap Java. Tornou-se um recurso final no JDK 22 e reside no pacote java.lang.foreign.

Este capítulo aborda como a memória fora do heap funciona no FFM, como um Arena controla seu tempo de vida, como layouts descrevem dados nativos e como chamar uma função C a partir do Java. Ao final, você deverá entender quando o FFM é a ferramenta certa e como suas peças se encaixam.

Por que o FFM Substitui o JNI

Antes do FFM, interagir com código nativo significava escrever manualmente glue code JNI, buffers de bytes manuais e um risco constante de travar a JVM com um ponteiro incorreto. Um único tipo incompatível ou deslocamento fora do limite poderia corromper o heap ou causar um segfault em todo o processo — e como o crash acontecia no código nativo, você não recebia nenhum stack trace Java.

O FFM substitui tudo isso com uma API pequena e type-safe construída em torno de três ideias:

  • Um Arena controla o tempo de vida da memória: quando fecha, tudo que alocou é liberado.
  • Um MemorySegment é uma visão com verificação de limites dessa memória, de modo que acessos fora do intervalo lançam uma exceção em vez de corromper a memória.
  • Um Linker constrói um handle chamável para uma função nativa, mapeando tipos C para tipos Java antecipadamente.

O resultado é que os erros surgem como exceções Java em tempo de link, não como crashes aleatórios mais tarde. O restante deste capítulo percorre cada peça individualmente.

Memória Fora do Heap com Arena e MemorySegment

Um MemorySegment é uma região contígua de memória com tamanho conhecido. Ao contrário de um array Java, ele pode viver fora do heap, de modo que o coletor de lixo nunca o move e ele pode ser entregue diretamente ao código nativo. Você nunca constrói um segmento diretamente — você pede um Arena por um, e a arena possui o tempo de vida do segmento.

Quando a arena fecha, todos os segmentos que ela alocou são liberados de uma vez. Isso torna os bugs de leak e use-after-free difíceis de escrever: toque em um segmento depois que sua arena fechar e você receberá uma exceção, não um crash.

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 leitura e escrita passa por um ValueLayout, que diz exatamente quantos bytes um valor ocupa e como é organizado. É isso que mantém cada acesso com verificação de limites e type-safe.

Escolhendo um Arena

Arena é o gerenciador de tempo de vida, e o método de fábrica escolhido decide quem pode acessar a memória e quando ela é liberada. Escolher o correto é a principal decisão de segurança no código FFM.

ArenaTempo de vidaAcesso por thread
Arena.ofConfined()Até close()Apenas a thread criadora
Arena.ofShared()Até close()Qualquer thread
Arena.ofAuto()Até o GC coletá-laQualquer thread
Arena.global()O programa inteiroQualquer thread

Use ofConfined() para o caso comum: memória de curta duração usada por uma thread e liberada deterministicamente com try-with-resources. Use ofShared() apenas quando várias threads precisam ler o mesmo segmento, e ofAuto() quando não é possível marcar facilmente o fim do tempo de vida. Se seu código usa virtual threads, prefira ofShared() ou ofAuto(), já que uma arena confinada está vinculada a uma thread portadora.

Descrevendo Layouts

Um ValueLayout descreve um único valor primitivo; um MemoryLayout pode descrever structs e arrays inteiros. Os layouts permitem calcular deslocamentos e tamanhos sem codificar números mágicos, o que mantém o acesso a structs nativas legível.

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
}

Os campos nomeados e os acessores PathElement significam que você descreve a struct uma vez e deixa a API calcular os deslocamentos de bytes para você.

Chamando Funções Nativas com Linker

O recurso principal do FFM é o downcall: invocar uma função C a partir do Java. Você obtém o Linker da plataforma, procura o endereço da função com um SymbolLookup, descreve sua assinatura com um FunctionDescriptor e recebe um MethodHandle que pode ser invocado como qualquer método 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
}

O FunctionDescriptor mapeia tipos C para carregadores Java: um ponteiro C torna-se ValueLayout.ADDRESS, um C size_t mapeia para JAVA_LONG, um C int para JAVA_INT. Acerte o mapeamento e a chamada é type-safe; erre e você aprende em tempo de link, não como um crash aleatório. Como as chamadas nativas escapam da rede de segurança da JVM, o FFM é uma operação restrita — o módulo que o usa deve receber acesso com o flag --enable-native-access.

Um Exemplo Completo e Executável

A API java.lang.foreign é um recurso de prévia antes do JDK 22, então o programa abaixo executa as mesmas duas ideias — memória fora do heap e manipulação de strings no estilo nativo — usando apenas as classes JDK sempre ativas que o FFM foi projetado para substituir. Um ByteBuffer direto é memória alocada fora do heap Java, assim como um MemorySegment; ler valores tipados em deslocamentos de bytes espelha um acesso ValueLayout; e varrer bytes até um terminador zero é exatamente o que strlen do C faz.

java— editable, runs on the server

O que observar na execução:

  • isDirect = true confirma que o buffer está alocado fora do heap Java — a mesma propriedade que permite que um MemorySegment seja passado com segurança ao código nativo sem o GC realocá-lo.
  • Escrever (i + 1) * 10 em cada deslocamento de 4 bytes e ler de volta produz 10, 20, 30, 40 com sum = 100, mostrando que a memória fora do heap é armazenamento real, indexável e tipado, assim como um MemorySegment.
  • byteSize = 16 são quatro ints de 4 bytes — endereçar por deslocamento explícito de bytes é exatamente como um ValueLayout calcula posições na API FFM real.
  • O cString construído à mão termina em um byte zero, então a varredura estilo strlen para lá: strlen of the C string = 16 corresponde a Java String.length() = 16, provando que o terminador nulo marca o fim como o C espera.
  • Nenhum buffer é liberado manualmente — buffers diretos são reclamados quando inacessíveis, espelhando Arena.ofAuto(), enquanto a arena real ofConfined() do FFM liberaria deterministicamente em close().

Quando Usar o FFM

O FFM é uma ferramenta especializada, não uma cotidiana. Use-o quando você realmente precisar de interoperabilidade nativa ou memória fora do heap:

  • Chamar uma biblioteca nativa existente — um codec de imagem em C, um driver de banco de dados, um SDK de hardware — sem escrever glue code JNI.
  • Compartilhar buffers grandes com código nativo onde copiar para o heap Java seria desperdiçador, como pipelines de gráficos ou áudio.
  • Trabalhar com conjuntos de dados muito grandes fora do heap que não devem pressionar o coletor de lixo.

Para trabalho comum com arquivos e buffers, fique com APIs de nível mais alto como Java NIO; elas são mais simples e seguras por padrão. E lembre-se que o FFM é uma operação restrita: como chamadas nativas escapam das garantias de segurança da JVM, você deve inicializar com --enable-native-access ou receberá um aviso ou erro em tempo de execução.

Prática

Prática
Na API FFM, qual é o papel de um Arena?
Na API FFM, qual é o papel de um Arena?
Was this page helpful?