Native Image 中的 Java Native Interface (JNI)

Java 本机接口 (JNI) 是一个本机 API,它使得 Java 代码能够与本机代码交互,反之亦然。本页面概述了 Native Image 中的 JNI 实现。

JNI 支持默认启用并内置于 Native Image 中。通过 JNI 可访问的单个类、方法和字段必须在镜像构建时在配置文件中指定(详见下文)。

Java 代码可以使用 System.loadLibrary() 从共享对象加载本机代码。或者,本机代码可以加载 JVM 的本机库并通过 Invocation API 附加到其 Java 环境。Native Image JNI 实现支持这两种方法。

目录 #

加载本机库 #

当使用 System.loadLibrary()(及相关 API)加载本机库时,本机镜像会先搜索包含本机镜像的目录,然后才搜索 Java 库路径。因此,只要要加载的本机库与本机镜像位于同一目录,就不需要其他设置。

反射元数据 #

JNI 支持按名称查找类,以及按名称和签名查找方法和字段。这需要保留这些查找所需的元数据。native-image 构建器必须事先知道哪些项将被查找,以防它们可能无法通过其他方式访问,因此不会包含在本地镜像中。此外,native-image 必须为任何可以通过 JNI 调用的方法提前生成包装代码。因此,指定需要通过 JNI 访问的项的简洁列表,可以保证它们的可用性并允许更小的占用空间。这样的列表应在 reachability-metadata.json 文件中指定。

JNI 配置可以使用 GraalVM JDK 中的 Tracing Agent 自动收集。该代理在常规 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 函数 FromReflectedMethodToReflectedMethod 可用于获取与 java.lang.reflect.Methodjava.lang.reflect.Constructor 对象对应的 jmethodID,反之亦然。函数 FromReflectedFieldToReflectedField 用于在 jfieldIDjava.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)

当镜像构建遇到声明为 native 的方法时,它会生成一个带有包装器的图,该包装器执行到本地代码和返回的转换,添加 JNIEnv*this 参数,将所有对象参数装箱到句柄中,如果返回类型是对象,则解箱返回的句柄。

实际的本机调用目标地址只能在运行时确定。因此,native-image 构建器还在 native 声明方法的反射元数据中创建了一个额外的链接对象。当调用本机方法时,调用包装器会在所有加载的库中查找匹配的符号,并将解析的地址存储在链接对象中,以供将来调用。另外,Native Image 也支持 RegisterNatives JNI 函数来显式提供本机方法的代码地址,而不是要求符号符合 JNI 名称修饰方案。

本机到 Java 的方法调用 #

本机代码可以通过首先获取目标方法的 jmethodID,然后使用 Call<Type>MethodCallStatic<Type>MethodCallNonvirtual<Type>Method 函数之一进行调用来调用 Java 方法。每个 Call... 函数也提供 Call...MethodACall...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 方法的签名,解箱任何传入的对象句柄,并在必要时将返回类型装箱到对象句柄中。

每个可以通过 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 在一个步骤中创建并初始化对象(或其变体 NewObjectANewObjectV)。例如:

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 支持这两种方法。构造函数必须以方法名 <init> 包含在 JNI 配置中。NewObject 不会生成额外的调用包装器,而是重用常规的 CallVoidMethod 包装器,并检测到它通过 NewObject 调用,因为它被传递了目标类的 Class 对象。在这种情况下,调用包装器会在调用实际构造函数之前分配一个新实例。

字段访问 #

本机代码可以通过获取其 jfieldID,然后使用 Get<Type>FieldSet<Type>FieldGetStatic<Type>FieldSetStatic<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 访问的字段,其在对象(或静态字段区域)中的偏移量存储在反射元数据中,并用作其 jfieldIDnative-image 构建器为所有基本类型和对象字段生成访问器方法。这些访问器方法执行到 Java 代码和返回的转换,并使用不安全的加载或存储来直接操作字段值。由于分析无法观察通过 JNI 对对象字段的赋值,因此它假定字段的声明类型的任何子类型都可以出现在可通过 JNI 访问的字段中。

JNI 还允许写入声明为 final 的字段,这必须通过配置文件中的 allowWrite 属性为单个字段启用。但是,由于优化,访问 final 字段的代码可能无法以与非 final 字段相同的方式观察 final 字段值的更改。

异常 #

JNI 规定,由于从本机代码调用而导致的 Java 代码中的异常必须被捕获并保留。在 Native Image 中,这是在本地到 Java 的调用包装器和 JNI 函数的实现中完成的。然后,本机代码可以使用 ExceptionCheckExceptionOccurredExceptionDescribeExceptionClear 函数查询和清除挂起的异常。本机代码还可以使用 ThrowThrowNewFatalError 抛出异常。当异常在本地代码中未被处理或本地代码本身抛出异常时,Java 到本地的调用包装器在重新进入 Java 代码时会重新抛出该异常。

监视器 #

JNI 声明了 MonitorEnterMonitorExit 函数以获取和释放对象的内在锁。Native Image 提供了这些函数的实现。然而,native-image 构建器只将内在锁直接分配给分析观察到在 Java synchronized 语句以及 wait()notify()notifyAll() 中使用的类的对象。对于其他对象,同步会回退到一种较慢的机制,该机制使用映射来存储锁关联,而这本身就需要同步。因此,将同步包装在 Java 代码中可能是有益的。

联系我们