JNI and the FFM Boundary
Purpose
Reason about the cost and the risk of a call that leaves the JVM. The boundary has three independent cost components — the thread-state transition, marshaling, and verification — and almost every wrong decision here comes from collapsing them into one number, or from treating an option that addresses one of them as if it addressed another.
The failure this prevents is the migration that fixes nothing. FFM can improve safety and
binding ergonomics without making a blocking foreign call unmountable. A native/foreign
frame prevents virtual-thread unmounting in current HotSpot; critical() is a narrowly
constrained optimization hint, not an asynchronous-native-call mechanism.
Workflow
Inspect the project's compiler/toolchain, runtime image, native library and target ABI first. The final FFM examples require JDK 22+; the reference observations use HotSpot 25.0.3. Applying this skill does not authorize an upgrade or removal of preview flags used by other features.
- Specify the native contract first. ABI, ownership, lifetime, thread affinity, reentrancy/upcalls, cancellation, error channel, blocking behavior and worst-case duration decide correctness. API choice also affects checks, maintainability and deployment.
- Amortise fixed cost by batching when the call is short and frequent: one transition for the whole batch, with the work loop inside the native code. This is a throughput technique and does nothing for pinning.
- Apply the documented
critical()preconditions. The function must be extremely short in every case and must not call back into Java. Prove bounded non-blocking behavior and benchmark the complete service; do not invent a universal microsecond cutoff. Seereferences/critical-and-decision-matrix.md. - Diagnose carrier capture with multiple signals.
jdk.VirtualThreadPinnedreports a virtual thread attempting a blocking operation while pinned; it may not report C code simply blocking inside a native frame. Combine JFR, thread dumps, wall/native profiles, call-duration metrics and carrier saturation. - Isolate or redesign blocking native calls: use a bounded dedicated platform-thread
pool, an asynchronous/non-blocking native API, process isolation or a Java alternative.
Size/admit the pool from latency, concurrency, resource limits and overload policy, then
let the virtual thread await the
Future. Waiting on aFutureis ordinary Java and unmounts normally. Allocate the call's segments on the pool thread, inside the task: a confined-arena segment created on the caller's thread fails the first downcall withWrongThreadException. Seereferences/arenas-upcalls-and-gc.md. - Declare native access explicitly in production.
--enable-native-access=<module>orALL-UNNAMED, per module, rather than relying on the current warn-only default. - Measure the boundary, do not estimate it. JMH comparing JNI, a plain FFM downcall and
a
criticalone for the same function.-prof gcmeasures Java allocation, not native copy volume; instrument bytes/copies or inspect the native implementation separately.
Return the boundary contract, observed evidence versus hypotheses, proposed change and the validation that would demonstrate correctness and the intended operational benefit.
Rules
- Current virtual-thread implementations cannot unmount across a native method or foreign function frame. JNI and FFM therefore both capture a carrier for blocking work; exact event visibility and stub behavior differ, so diagnose rather than assuming identical telemetry.
Linker.Option.isTrivial()does not exist in the finalised FFM API. The final name, since JEP 454 (JDK 22 GA), isLinker.Option.critical(boolean allowHeapAccess).critical()is an API hint that permits implementation optimizations valid only for an extremely short, no-upcall function. HotSpot versions may omit normal transitions/checks, increasing safepoint and crash risk if preconditions are violated. Do not encode a specific_thread_in_nativeimplementation as the portable contract.- Read
criticalas "critical section", not as "trivial" or "fast and always safe". That misreading is the most common error with this API. critical(true)permits heap-backed segments as address arguments for the call. It is conceptually related to JNI critical access but not an exact equivalence: JNI may return a copy or pin, and FFM/collector implementation can evolve. Treat the address as temporary, obey critical-section restrictions, and observe collector/safepoint behavior on the deployed JDK. Seereferences/arenas-upcalls-and-gc.md.- An exception escaping an upcall target terminates the JVM, per the
Linkercontract. Every upcall target catchesThrowableand translates it into a return code, and no upcall may run from acriticaldowncall. Seereferences/arenas-upcalls-and-gc.md. - Mitigate blocking native calls with bounded platform-thread isolation, a truly asynchronous
native interface, process isolation or replacement.
critical()and a JNI-to-FFM rewrite alone do not make the call unmountable. -Djdk.tracePinnedThreadswas removed in JDK 24. Usejdk.VirtualThreadPinnedfor Java blocking attempts while pinned, plus wall/native profiles and carrier/call metrics for time spent blocking inside native code.- JEP 472 brought JNI loading under the native-access restrictions already used by FFM in
JDK 24. On JDK 24/25, unauthorized restricted use warns by default and can be configured;
future policy is intended to deny. Declare
--enable-native-accessfor the actual calling modules and test with the exact release's--illegal-native-accesspolicy. - Warnings are associated with restricted load/link operations such as native library loads, downcall/upcall creation and library lookup, typically once per caller module—not each segment read. Generated bindings do not inherit an exemption; attribution follows the module that invokes the restricted operation.
- FFM is final since JDK 22 and does not itself require
--enable-previewthere. Older preview APIs differ; preserve flags needed by other project features. jextractis an OpenJDK project/tool distributed separately from the standard JDK; vendor bundles can differ. Pin its version/target ABI and review generated ownership/error policy.- Close confined/shared arenas according to the native ownership boundary. Automatic arenas need a strongly reachable Java owner while native code retains pointers. The global arena remains alive for the JVM lifetime, trading simple retention for no early reclamation. Neither kind is manually closeable. JNI critical/element APIs must be released on every path.
- Do not assume FFM is faster than JNI. Descriptor shape, checks, marshaling, JIT compilation, native work and copies dominate differently. Benchmark the same ABI/function/data path and retain safety and maintainability in the decision.
- An aggregate CPU overhead calculation is not a tail-latency prediction without an explicit queueing model connecting the two.
- A
FunctionDescriptoris executable ABI metadata. Wrong C width, signedness, struct layout, variadic boundary, calling convention or callback lifetime can corrupt memory or crash the JVM despite Java's static types. Test against headers on every target platform. - Native cancellation is cooperative: cancelling a
Futureor interrupting the Java caller does not reliably stop C code. Define timeout, abandonment, resource ownership and late completion behavior at the boundary.
References
- Critical, and choosing an interop approach —
the overhead components per call type, the thread-state and safepoint table, the measurable
eligibility criteria for
critical(), and the JNI/Panama/jextract/JNA decision matrix. Read before choosing an interop API or approving acritical()call. - Arenas, upcalls and the collector — arena kinds at
the interop boundary (confined handoff, shared close, stub lifetime, automatic arenas),
the upcall contracts and cost order,
captureCallStateforerrnoandfirstVariadicArg, what a critical region does to each collector, and the testing levers. Read when a downcall fails withWrongThreadExceptionorAlready closed, when designing a callback API, when a native function setserrnoor is variadic, or whenGCLocker Initiated GCappears in a GC log. - Detecting and mitigating native pinning — the JFR
and async-profiler recipes for pinning of native origin, the dedicated-pool mitigation
pattern, the JEP 472 warning surface,
jextractusage, and the operational checklists. Read during an incident, or before a service that makes native calls goes to production.