- 适用于 JDK 23 的 GraalVM(最新)
- 适用于 JDK 24 的 GraalVM(抢先体验版)
- 适用于 JDK 21 的 GraalVM
- 适用于 JDK 17 的 GraalVM
- 存档
- 开发版本
Java 本地接口 (JNI) 在 Native Image 中
Java 本地接口 (JNI) 是一个本地 API,它使 Java 代码能够与本地代码交互,反之亦然。本页概述了 Native Image 中的 JNI 实现。
JNI 支持默认情况下已启用并内置于 Native Image 中。在映像构建时,必须在配置文件中指定应通过 JNI 访问的各个类、方法和字段(请阅读以下内容)。
Java 代码可以使用 System.loadLibrary()
从共享对象加载本地代码。或者,本地代码可以加载 JVM 的本地库并使用调用 API 附加到其 Java 环境。Native Image JNI 实现支持这两种方法。
目录 #
加载本地库 #
使用 System.loadLibrary()
(以及相关 API)加载本地库时,本地映像将在搜索 Java 库路径之前搜索包含本地映像的目录。因此,只要要加载的本地库与本地映像位于同一目录中,则不需要其他设置。
反射元数据 #
JNI 支持通过名称查找类,并通过名称和签名查找方法和字段。这需要保留这些查找所需的元数据。native-image
构建器必须预先知道哪些项目将被查找,以防它们可能无法访问,因此不会包含在本地映像中。此外,native-image
必须为可以通过 JNI 调用的任何方法提前生成包装器代码。因此,指定需要通过 JNI 访问的项目的简洁列表可以保证它们的可用性并允许更小的占用空间。这样的列表应在reachability-metadata.json 文件中指定。
可以使用 GraalVM JDK 中的跟踪代理自动收集 JNI 配置。代理跟踪应用程序在普通 Java VM 上执行期间所有动态功能的使用情况。当应用程序完成并且 JVM 退出时,代理将配置写入指定输出目录中的 JSON 文件。如果您将生成的配置文件从该输出目录移动到类路径上的 META-INF/native-image/,它们将在构建时自动使用。native-image
构建器搜索 META-INF/native-image/ 及其子目录以查找名为 reachability-metadata.json 的文件,或诸如 reflect-config.json 等旧文件。
或者,自定义 Feature
实现可以在图像构建的分析阶段之前和期间使用 JNIRuntimeAccess
类注册程序元素。例如
class JNIRegistrationFeature implements Feature {
public void beforeAnalysis(BeforeAnalysisAccess access) {
try {
JNIRuntimeAccess.register(String.class);
JNIRuntimeAccess.register(String.class.getDeclaredField("value"));
JNIRuntimeAccess.register(String.class.getDeclaredField("hash"));
JNIRuntimeAccess.register(String.class.getDeclaredConstructor(char[].class));
JNIRuntimeAccess.register(String.class.getDeclaredMethod("charAt", int.class));
JNIRuntimeAccess.register(String.class.getDeclaredMethod("format", String.class, Object[].class));
JNIRuntimeAccess.register(String.CaseInsensitiveComparator.class);
JNIRuntimeAccess.register(String.CaseInsensitiveComparator.class.getDeclaredMethod("compare", String.class, String.class));
} catch (NoSuchMethodException | NoSuchFieldException e) { ... }
}
}
要激活自定义功能,请将 --features=<JNIRegistrationFeature 类的完全限定名>
传递给 native-image
构建器。Native Image 构建配置 解释了如何使用 META-INF/native-image
中的 native-image.properties
文件自动执行此操作。
java.lang.reflect 支持 #
JNI 函数 FromReflectedMethod
和 ToReflectedMethod
可用于获取与 java.lang.reflect.Method
或 java.lang.reflect.Constructor
对象相对应的 jmethodID
,反之亦然。函数 FromReflectedField
和 ToReflectedField
在 jfieldID
和 java.lang.reflect.Field
之间转换。为了使用这些函数,必须启用反射支持,并且所讨论的方法和字段必须包含在反射元数据中。
对象句柄 #
JNI 不允许直接访问 Java 对象。相反,JNI 提供了字大小的对象句柄,可以将其传递给 JNI 函数以间接访问对象。本地句柄仅在本地调用持续时间内并且仅在调用者的线程中有效,而全局句柄跨线程有效,并且在明确销毁之前保持有效。句柄 0 代表 NULL
。
Native Image 使用线程本地、不断增长的引用对象数组来实现本地句柄,其中数组中的索引是句柄值。“指针”指向将分配下一个句柄的位置。本地调用可以嵌套,因此在调用本地方法之前,调用存根将当前指针推送到堆栈上,并在其返回后,它从堆栈中恢复旧指针并将数组中所有来自调用的对象引用归零。
全局句柄使用可变数量的对象数组来实现,这些对象数组使用原子操作插入和归零引用对象。全局句柄的值是一个负整数,该整数由包含数组的索引和数组中的索引确定。因此,JNI 代码可以通过仅查看其符号来区分本地句柄和全局句柄。分析不会因对象句柄而受阻,因为它可以观察到对象引用的整个流程,并且传递给本地代码的句柄仅是数值。
Java 到本地方法调用 #
用 native
关键字声明的方法在本地代码中具有符合 JNI 的实现,但可以像任何其他 Java 方法一样调用。例如
// Java declaration
native int[] sort0(int[] array);
// native declaration with JNI name mangling
jintArray JNICALL Java_org_example_sorter_IntSorter_sort0(JNIEnv *env, jobject this, jintArray array)
当映像构建遇到声明为本地的某个方法时,它会生成一个图形,该图形包含执行从本地代码到本地代码的转换的包装器,添加 JNIEnv*
和 this
参数,将任何对象参数装箱到句柄中,并且在对象返回类型的情况下,将返回的句柄拆箱。
实际的本地调用目标地址只能在运行时确定。因此,native-image
构建器还在本地声明方法的反射元数据中创建了一个额外的链接对象。当调用本地方法时,调用包装器在所有已加载的库中查找匹配的符号,并将解析后的地址存储在链接对象中以供将来调用。或者,Native Image 还可以支持 RegisterNatives
JNI 函数来显式提供本地方法的代码地址,而不是要求符合 JNI 名称修饰方案的符号。
本地到 Java 方法调用 #
本地代码可以通过首先获取目标方法的 jmethodID
,然后使用 Call<Type>Method
、CallStatic<Type>Method
或 CallNonvirtual<Type>Method
函数之一来调用 Java 方法。每个 Call...
函数都可以在 Call...MethodA
和 Call...MethodV
变体中使用,它们以数组或 va_list
而不是可变参数的形式接受参数。例如
jmethodID intcomparator_compare_method = (*env)->GetMethodID(env, intcomparator_class, "compare", "(II)I");
jint result = (*env)->CallIntMethod(env, this, intcomparator_compare_method, a, b);
native-image
构建器根据提供的 JNI 配置为可以通过 JNI 调用的每个方法生成调用包装器。调用包装器符合适用于该方法的 JNI Call...
函数的签名。包装器执行从 Java 代码到 Java 代码的转换,将参数列表调整为目标 Java 方法的签名,拆箱任何传递的对象句柄,并在必要时将返回值装箱到对象句柄中。
每个可以通过 JNI 调用的方法都具有一个反射元数据对象。该对象的地址用作该方法的 jmethodID
。元数据对象包含该方法所有生成的调用包装器的地址。由于每个调用包装器都精确符合相应的 Call...
函数签名,因此 Call...
函数本身只不过是根据传递的 jmethodID
无条件跳转到适当的调用包装器。作为另一种优化,调用包装器能够从 JNIEnv*
参数中有效地恢复当前线程的 Java 上下文。
JNI 函数 #
JNI 提供了一组函数,本地代码可以使用这些函数与 Java 代码交互。Native Image 使用 @CEntryPoint
实现这些函数,例如
@CEntryPoint(...) private static void DeleteGlobalRef(JNIEnvironment env, JNIObjectHandle globalRef) { /* setup; */ JNIGlobalHandles.singleton().delete(globalRef); }
JNI 指定这些函数是通过 C 结构中的函数指针提供的,该函数指针可以通过 JNIEnv*
参数访问。此结构的自动初始化是在映像构建期间准备的。
对象创建 #
JNI 提供两种创建 Java 对象的方法,要么调用 AllocObject
来分配内存,然后使用 CallVoidMethod
调用构造函数,要么使用 NewObject
来一步创建和初始化对象(或 NewObjectA
或 NewObjectV
变体)。例如
jclass calendarClass = (*env)->FindClass(env, "java/util/GregorianCalendar");
jmethodID ctor = (*env)->GetMethodID(env, calendarClass, "<init>", "(IIIIII)V");
jobject firstObject = (*env)->AllocObject(env, calendarClass);
(*env)->CallVoidMethod(env, obj, ctor, year, month, dayOfMonth, hourOfDay, minute, second);
jobject secondObject = (*env)->NewObject(env, calendarClass, ctor, year, month, dayOfMonth, hourOfDay, minute, second);
Native Image 支持这两种方法。构造函数必须包含在 JNI 配置中,其方法名为 <init>
。NewObject
不生成额外的调用包装器,而是重用常规的 CallVoidMethod
包装器,并在通过 NewObject
调用它时检测到它,因为它传递了目标类的 Class
对象。在这种情况下,调用包装器在调用实际构造函数之前分配一个新实例。
字段访问 #
本地代码可以通过获取其 jfieldID
,然后使用 Get<Type>Field
、Set<Type>Field
、GetStatic<Type>Field
或 SetStatic<Type>Field
函数之一来访问 Java 字段。例如
jfieldID intsorter_comparator_field = (*env)->GetFieldID(env, intsorter_class, "comparator", "Lorg/example/sorter/IntComparator;");
jobject value = (*env)->GetObjectField(env, self, intsorter_comparator_field);
对于可以通过 JNI 访问的字段,其在对象(或静态字段区域)中的偏移量存储在反射元数据中,并用作其 jfieldID
。native-image
构建器为所有基本类型和对象字段生成访问器方法。这些访问器方法执行从 Java 代码到 Java 代码的转换,并使用不安全的加载或存储来直接操作字段值。由于分析无法观察到通过 JNI 对对象字段的赋值,因此它假定字段声明类型的任何子类型都可能出现在可以通过 JNI 访问的字段中。
JNI 还允许写入声明为 final
的字段,这些字段必须通过配置文件中的 allowWrite
属性为各个字段启用。但是,访问 final 字段的代码可能无法像访问非 final 字段那样观察到 final 字段值的更改,因为存在优化。
异常 #
JNI 规范规定,从原生代码调用产生的 Java 代码异常必须被捕获并保留。在 Native Image 中,这在原生到 Java 的调用包装器和 JNI 函数的实现中完成。原生代码随后可以使用 `ExceptionCheck`、`ExceptionOccurred`、`ExceptionDescribe` 和 `ExceptionClear` 函数查询和清除挂起的异常。原生代码还可以使用 `Throw`、`ThrowNew` 或 `FatalError` 抛出异常。当异常在原生代码中未被处理或原生代码本身抛出异常时,Java 到原生代码的调用包装器会在重新进入 Java 代码时重新抛出该异常。
监视器 #
JNI 声明了函数 `MonitorEnter` 和 `MonitorExit` 来获取和释放对象的内在锁。Native Image 提供了这些函数的实现。但是,`native-image` 构建器仅直接将内在锁分配给分析可以观察到在 Java `synchronized` 语句中以及使用 `wait()`、`notify()` 和 `notifyAll()` 的类的对象。对于其他对象,同步将回退到使用映射来存储锁关联的较慢机制,该机制本身需要同步。因此,在 Java 代码中包装同步可能是有益的。