JNI 概览
JNI 如何让 Java 调用本地代码:适用场景、安全与可移植性代价。
#concept
#growing
#dev
#java
[!info] related notes
JNI 概览
这个概念为什么出现
极少数场景必须调用 OS API、硬件 SDK 或既有 C/C++ 库。JNI 是经典互操作桥,但破坏 WORA 的“无本地假设”,并引入崩溃与内存风险。
[!abstract] 一句话理解 JNI 通过
native方法把调用转到本地库;用可移植性与安全边界换取本地能力,应评估本地边界,并在需要进程故障隔离时采用进程外方案。
最小可观察示例
场景:声明 native 方法(不链接真实库,仅展示形态)
public class NativeTime {
public native long monotonicNanos();
public static void main(String[] args) {
System.out.println("jni-boundary-demo");
}
}
javac -h headers NativeTime.java && java NativeTime
期望:
jni-boundary-demo
此命令还生成 headers/NativeTime.h;main 未调用 native 方法,所以无需真实库。若直接调用未绑定的 monotonicNanos,会抛 UnsatisfiedLinkError。真实调用还需 C/C++ 实现、相匹配的本地二进制及加载/注册步骤。
结合场景再看三个关注点
- 本地崩溃可拖垮 JVM 进程。
- 每平台交付本地
.so/.dll/.dylib。 - 优先查是否有纯 Java 或 FFM 新方案。
核心概念与准确模型
1. 边界
Java 声明 native 方法,本地实现可按 JNI 命名规则导出符号,也可用 RegisterNatives 显式注册。System.loadLibrary 接收逻辑库名(如 native_time),由运行时按平台规则寻找;System.load 接收本地库绝对路径。
2. 引用与临界区
JNI 引用不是可长期保存的裸对象地址。局部引用与全局引用有不同生命周期,全局引用需 DeleteGlobalRef 释放;JNIEnv 属于当前线程,不能跨线程共享。获取数组/字符串数据后需配对释放,并遵守相应临界区限制,不能用一句“防 GC 移动”替代这些规则。
3. 替代
- 进程外 sidecar
- Foreign Function & Memory API:JEP 454 在 JDK 22 正式交付;JDK 21 的版本仍是 preview。它提供另一条互操作路径,不自动消除本地代码导致的进程崩溃。
边界情况与反直觉行为
- 难以调试的 mixed stack。
- 容器镜像架构(amd64/arm64)必须匹配。
- 库缺失、CPU/ABI 不匹配、依赖库缺失都可能导致加载失败。另需按目标 JDK 核对 native access 策略:JEP 472 在 JDK 24 默认对未授权的 JNI native access 发出警告,可按模块配置
--enable-native-access;不要把历史 SecurityManager 策略当作新 JDK 的统一模型。
常见误区
[!warning] 常见误区:为了“更快”先写 JNI 多数热点应先算法与分配,再测;JNI 调用本身也有成本。
工程实践
- 本地代码最小化表层。
- 严格 CI 多架构。
- 崩溃处理与可观测。
- 文档写清非纯 Java 依赖。
本节总结
- JNI=本地桥
- 可移植性与安全代价高
- 最后手段
自测题
- JNI 如何影响 WORA?
- 为何容器要多架构构建本地库?
参考答案
- 引入平台相关二进制,不再“只发 jar 即可到处跑”。
- CPU 架构不同,本地指令与 ABI 不通用。
延伸阅读与资料来源
| 资料 | 类型 | 支撑内容 |
|---|---|---|
| JNI Specification | 规范 | JNI |
| FFM API | JEP | JDK 22 正式交付 |
| Prepare to Restrict the Use of JNI | JEP | JDK 24 native access 策略 |