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
Arenacontrola 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
Linkerconstró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 hereCada 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.
| Arena | Tempo de vida | Acesso por thread |
|---|---|---|
Arena.ofConfined() | Até close() | Apenas a thread criadora |
Arena.ofShared() | Até close() | Qualquer thread |
Arena.ofAuto() | Até o GC coletá-la | Qualquer thread |
Arena.global() | O programa inteiro | Qualquer 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.
O que observar na execução:
isDirect = trueconfirma que o buffer está alocado fora do heap Java — a mesma propriedade que permite que umMemorySegmentseja passado com segurança ao código nativo sem o GC realocá-lo.- Escrever
(i + 1) * 10em cada deslocamento de 4 bytes e ler de volta produz10, 20, 30, 40comsum = 100, mostrando que a memória fora do heap é armazenamento real, indexável e tipado, assim como umMemorySegment. byteSize = 16são quatro ints de 4 bytes — endereçar por deslocamento explícito de bytes é exatamente como umValueLayoutcalcula posições na API FFM real.- O
cStringconstruído à mão termina em um byte zero, então a varredura estilo strlen para lá:strlen of the C string = 16corresponde aJava 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 realofConfined()do FFM liberaria deterministicamente emclose().
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.