Every JVM project that touches native code hits the same wall. You need one function from a C library — getpid, a hardware SDK, a codec, a GPU driver — and the official answer is JNI: write a C header, write a C implementation, compile it per platform, ship a .so/.dll/.dylib for every OS and architecture, then debug segmentation faults in a language your team does not write.

So people reach for the shortcuts. JNA has been that shortcut since 2007. JNR-FFI was the faster shortcut. And since JDK 22, the JVM ships its own answer in the standard library: the Foreign Function & Memory API, project Panama, no third-party dependency at all.

In 2026 the interesting question is no longer “which one works” — all three work. It is “which one should a new project use, and when is the old choice still correct”.

JNA — Java Native Access

TL;DR / Quick Verdict

New project on JDK 22 or newer? Use Project Panama (FFM). It is in java.lang.foreign, it has no dependencies, it is the fastest of the three once warmed up, and it is the only option that will still be the recommended path in five years.

Stuck on JDK 8, 11 or 17 and cannot move? Use JNA if you want zero build complexity, JNR-FFI if call throughput actually matters. JNA binds an interface by reflection at load time; JNR generates bytecode stubs and is meaningfully faster per call. Both are mature and both are still maintained.

Writing a library that other people will consume on unknown JDKs? Ship JNA. It is the lowest common denominator that runs from Java 8 to 25 without a build step, and that compatibility is worth more than the performance you give up.

DimensionJNAJNR-FFIProject Panama (FFM)
Repositoryjava-native-access/jnajnr/jnr-ffiopenjdk/panama-foreign (in the JDK)
Stars8,9351,327348 (spec repo, not the impl)
Last push2026-11-022026-11-022026-10-30
Latest version5.17.02.2.17Part of JDK 22+
LicenseLGPL 2.1 / Apache 2.0 dualApache 2.0GPLv2 + Classpath Exception
DependencyOne JARSeveral JARs (ffi, asm)None
Dispatch mechanismReflective proxyGenerated bytecode stubsMethodHandle + Linker
Native headers neededNoNoNo (or jextract)
JDK floorJava 8Java 8Java 22
Memory modelMemory / Structure (GC-managed)Pointer (GC-managed)Arena + MemorySegment (explicit)

Decision Matrix

SituationPickReason
Greenfield service on JDK 21/25 LTSPanama FFMStdlib, fastest, no supply-chain surface
Library used by strangers on many JDKsJNABroadest compatibility, no build step
Millions of native calls per secondPanama FFMMethod-handle dispatch, no per-call reflection
Same, but JDK 17 with no upgrade pathJNR-FFIBytecode stubs, much lower per-call cost than JNA
One-off call at startup (getpid, hostname)JNASetup is three lines; speed is irrelevant
Struct-heavy C API (gRPC, io_uring, drivers)Panama FFM + jextractGenerated layouts beat hand-written Structure classes
Long-running native callbacks into JavaPanama FFM upcall stubsFirst-class upcalls, explicit arena lifetime
Cannot add native-access flagsJNAFFM triggers restricted-method warnings without them

JNA — The Compatibility Play

JNA is the reason most JVM developers have never written a line of JNI. You declare a Java interface whose method names match C symbols, and JNA builds a reflective proxy that looks up the symbol and marshals arguments at call time.

8,935 stars, last pushed 2026-11-02, version 5.17.0, dual LGPL 2.1 / Apache 2.0. Nearly two decades of production use, and bindings exist for everything from Win32 to libvirt.

1
2
3
4
5
<dependency>
  <groupId>net.java.dev.jna</groupId>
  <artifactId>jna</artifactId>
  <version>5.17.0</version>
</dependency>
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
import com.sun.jna.Library;
import com.sun.jna.Native;

public interface LibC extends Library {
    LibC INSTANCE = Native.load("c", LibC.class);

    int getpid();
    long strlen(String s);
}

// usage
int pid = LibC.INSTANCE.getpid();

Structs are plain classes extending Structure. Note the explicit field order — JNA needs it, and getting it wrong produces silent garbage rather than an exception:

1
2
3
4
5
6
7
import com.sun.jna.Structure;

@Structure.FieldOrder({"tvSec", "tvUsec"})
public class Timeval extends Structure {
    public long tvSec;
    public long tvUsec;
}

Callbacks work, but this is where JNA’s reflective design starts to cost you: every invocation crosses a proxy, and struct-by-value arguments are copied field by field. A C API that returns a struct by value in a hot loop is the classic case where a JNA binding becomes the bottleneck and someone rewrites it in JNR.

When JNA is still the right answer: distribution. If your code ships as a library and you do not know the consumer’s JDK, JNA is the only one of the three that will load everywhere from Java 8 upward with a single dependency and no compiler flags. That is also why so many infrastructure tools (log shippers, hardware SDK wrappers, desktop apps) still bundle it.

JNR-FFI — Faster Stubs, Same Idea

JNR was written to fix JNA’s per-call overhead. Instead of a reflective proxy, JNR generates a Java class at load time with ASM that binds directly to the native function pointer. The programming model stays almost identical, which makes it an easy migration.

1,327 stars, last pushed 2026-11-02, version 2.2.17, Apache 2.0.

1
2
3
4
5
<dependency>
  <groupId>com.github.jnr</groupId>
  <artifactId>jnr-ffi</artifactId>
  <version>2.2.17</version>
</dependency>
1
2
3
4
5
6
7
8
9
import jnr.ffi.LibraryLoader;

public interface LibC {
    int getpid();
    long strlen(String s);
}

LibC libc = LibraryLoader.create(LibC.class).load("c");
System.out.println(libc.getpid());

The interface does not extend anything and there is no INSTANCE field — the loader returns the implementation. That is the whole migration: drop the Library supertype, replace Native.load with LibraryLoader.create(...).load(...).

JNR also exposes jnr.ffi.Pointer, Runtime, and Struct for lower-level work, plus bindings to POSIX (jnr-posix) that some projects use instead of shelling out. Its practical downside is dependency surface: jnr-ffi pulls in ASM and a handful of small JNR modules, which matters if you are already fighting a large dependency graph or an offline build.

When JNR is the right answer: you are on JDK 17 or 11, you cannot adopt FFM yet, and profiling says native dispatch is your problem. JNR is also a reasonable choice for projects that already depend on the JNR ecosystem (JRuby does, for instance).

Project Panama — The Standard Library Answer

The Foreign Function & Memory API finalized in JDK 22 (JEP 454) after years of preview. It replaces both sun.misc.Unsafe off-heap tricks and third-party FFI with three core ideas: a Linker that knows how to call native code, a SymbolLookup that resolves symbols, and an Arena that owns the lifetime of native memory.

No dependency. That is the headline. java.lang.foreign is in the JDK, which means no version conflicts, no shaded JARs, no CVE feed for your FFI layer.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
import java.lang.foreign.*;
import java.lang.invoke.MethodHandle;

public class Panama {
    public static void main(String[] args) throws Throwable {
        Linker linker = Linker.nativeLinker();
        SymbolLookup lookup = linker.defaultLookup();

        MethodHandle getpid = linker.downcallHandle(
            lookup.find("getpid").orElseThrow(),
            FunctionDescriptor.of(ValueLayout.JAVA_INT));

        MethodHandle strlen = linker.downcallHandle(
            lookup.find("strlen").orElseThrow(),
            FunctionDescriptor.of(ValueLayout.JAVA_LONG, ValueLayout.ADDRESS));

        try (Arena arena = Arena.ofConfined()) {
            MemorySegment str = arena.allocateFrom("hello native world");
            int pid = (int) getpid.invokeExact();
            long len = (long) strlen.invokeExact(str);
            System.out.println("pid=" + pid + " len=" + len);
        }
    }
}

Three things to notice. First, invokeExact is used because method handles are polymorphic — with a mismatched signature you get a WrongMethodTypeException rather than silently wrong data, which is a real improvement over JNI. Second, Arena.ofConfined() ties the segment’s lifetime to the enclosing block; the memory is released deterministically at the closing brace, not whenever the GC feels like it. Third, nothing needs compiling.

For large C headers, do not hand-write FunctionDescriptors. Use jextract from the OpenJDK Panama build to generate the whole binding layer:

1
2
3
4
jextract --output src/main/java \
  -t org.example.libc \
  -l c \
  /usr/include/stdlib.h

That produces MemorySegment-returning stubs with layout classes derived from the actual header — including struct offsets and bitfields, which is exactly where hand-written bindings go wrong.

The operational details people trip over:

  • JDK 24 and later warn when Linker is used without --enable-native-access=ALL-UNNAMED. It is a warning today, and the flag exists to make native access explicit. Add it to your java invocation or MANIFEST attributes now instead of discovering it during an upgrade.
  • Structured concurrency plays well with arenas. Arena.ofConfined() cannot be shared across threads; Arena.ofShared() can, and closing it while another thread still holds a segment throws IllegalStateException instead of corrupting memory.
  • Upcalls (native code calling back into Java) are first-class: linker.upcallStub(methodHandle, descriptor, arena). This is the feature that finally makes C libraries with event callbacks practical without writing JNI.

Pitfalls That Cost Real Hours

A wrong FunctionDescriptor does not throw — it corrupts memory. Declaring a function returning int as returning JAVA_LONG on some ABIs works, on others silently reads garbage. Always generate descriptors with jextract when a header exists, and always write a test that calls a known function (getpid, strlen) before wiring up the complex one.

JNA struct field order is positional, not named. @Structure.FieldOrder must list fields in exactly the same order as the C struct. Mismatched order compiles, loads, and returns nonsense.

Passing Java String to a char* argument works only for read-only parameters. For anything that writes into the buffer, allocate memory explicitly (arena.allocate(n) in FFM, Memory in JNA) and read it back after the call.

Native crashes take the whole JVM with them. There is no exception for a segfault. Keep -XX:+CreateCoredumpOnCrash, -XX:ErrorFile=/var/log/java/hs_err_pid%p.log, and -XX:+PreserveFramePointer set on any JVM that loads native code, and reproduce with the smallest possible call site.

JNA’s reflective dispatch is slower than people expect. A JNA call historically costs hundreds of nanoseconds versus a handful for a method handle. If you are calling a native function per network packet or per log line, this is your bottleneck, and no amount of -XX tuning fixes it.

Arena closes are not optional. Letting Arena.ofConfined() escape its block and using a segment afterwards fails fast, but leaving arenas open in a long-lived pool leaks native memory that only shows up in RSS, not in the Java heap.

Do not mix FFM and JNI-based libraries for the same native library. Two bindings loading the same .so with different global state is a debugging nightmare that ends in hs_err files.

If you are generating binding code at build time, the same discipline as bytecode generation applies — see our JVM bytecode engineering comparison for how Byte Buddy, Javassist and ASM differ in generated-code debugging. If your reason for going native is startup time rather than C libraries, our GraalVM vs OpenJ9 vs HotSpot comparison covers the runtime trade-offs. And if the native library you are binding is a database driver, Testcontainers for Java, Go and Python will save you from testing against a shared instance.

FAQ

Is Project Panama a replacement for JNA and JNR?

Functionally yes, for code that can require JDK 22 or newer. Practically, JNA and JNR will remain in use for years because a large installed base still runs on JDK 8, 11 and 17, and because JNA’s single-JAR compatibility is hard to beat for libraries distributed to unknown runtimes.

Which is fastest: JNA, JNR, or Panama?

Panama is fastest in steady state because calls become direct method-handle invocations after linking, with no reflection and no generated-proxy layer. JNR sits in the middle thanks to its generated bytecode stubs. JNA’s reflective proxy is the slowest per call. The gap is only visible in hot loops — for a handful of calls at startup, all three are equivalent.

Do I still need JNI in 2026?

Only when you need something the FFI APIs cannot express — for example instrumenting the JVM internals, replacing a JVM class natively, or working with C++ APIs where no C shim exists. Even then, the common pattern is a thin C wrapper plus Panama rather than a full JNI layer.

What JDK do I need for the Foreign Function & Memory API?

It finalized in JDK 22 (JEP 454). JDK 21 and earlier only have it as a preview feature requiring --enable-preview, which is unsuitable for production. If you are pinned to 17, choose JNR or JNA instead of trying to run preview APIs.

Why do I get a warning about restricted methods after upgrading the JDK?

From JDK 24 onward, using Linker without --enable-native-access emits a warning on first use. Add --enable-native-access=ALL-UNNAMED for classpath applications, or name your module explicitly. It is a warning today, but the direction is clear: native access must be declared.

Can I use Panama with C++ libraries?

Not directly — the FFM API binds C ABI symbols only, and C++ name mangling plus classes are outside its model. The standard approach is to compile a small extern "C" shim that exposes plain functions and opaque pointers, then bind that shim with Panama. It is still far less work than a hand-written JNI bridge.

Does JNA work on GraalVM native image?

Yes, with reflection configuration for the interfaces you bind. This is one of the practical arguments for JNA in native-image projects: the bindings and the required metadata are well understood, whereas FFM support in native image has been evolving and should be validated against your specific GraalVM version before you commit.


💰 想测试你的市场判断力?我用 Polymarket 做预测市场交易——这是全球最大的预测市场平台,从大选结果到技术监管时间线,什么都可以押注。和赌博不同,这是真正的信息市场:你懂的信息越多,胜率越高。我靠预测技术相关事件的走向已经赚了不少。用我的邀请链接注册:Polymarket.com