翻译自 https://docs.oracle.com/javase/7/docs/technotes/guides/jni/spec/design.html
Chapter 2
本章重点介绍JNI中的主要设计问题。 本节中的大多数设计问题都与本地方法有关。 第5章将介绍调用API的相关设计。
JNI Interface Functions and Pointers
本地代码通过调用JNI函数来访问Java VM功能。 JNI函数可通过接口指针使用。 接口指针是指向指针的指针。 该指针指向一个指针数组,每个指针指向一个接口函数。 每个接口函数都在数组内的预定义偏移处。 图2-1展示了接口指针的组织结构。
Figure 2-1 Interface Pointer
JNI接口的组织方式类似于C ++虚函数表或COM接口。 使用接口表而不是固定函数集的优点是,JNI名称空间与本地代码分离。 VM可以轻松提供多个版本的JNI功能表。 例如,VM可能支持两个JNI功能表:
JNI接口指针仅在当前线程中有效。 因此,本地方法不得将接口指针从一个线程传递到另一个线程。 实现JNI的VM可以在JNI接口指针所指向的区域中分配和存储线程局部数据。
本地方法接收JNI接口指针作为参数。当从同一Java线程对本地方法进行多次调用时, VM保证VM将相同的接口指针传递给本地方法。 无论如何,本地方法可以在不同的Java线程调用,因此可以接收不同的JNI接口指针。
Compiling, Loading and Linking Native Methods
由于Java VM是多线程的,因此本地方法库也应被编译,并与支持多线程的本地编译器链接。 例如,对于使用Sun Studio编译器编译的C ++代码,应使用-mt 标志。 对于符合GNU gcc编译器的代码,应使用标志 -D_REENTRANT或-D_POSIX_C_SOURCE。 有关更多信息,请参阅本地编译器文档。
本机方法随System.loadLibrary方法一起加载。 在以下示例中,类初始化方法加载特定平台的本地库,其中定义了本地方法f:
package pkg;
class Cls {
native double f(int i, String s);
static {
System.loadLibrary(“pkg_Cls”);
}
}
System.loadLibrary的参数是库名。系统遵循标准但特定于平台的方法,将库名转换为本地库名。例如,Solaris系统将名称pkg_Cls转换为libpkg_Cls.so,而Win32系统将相同的pkg_Cls名称转换为pkg_Cls.dll。
程序员可以使用单个库来存储任意数量的类所需的所有本地方法,只要这些类要使用同一类加载器加载即可。 VM在内部维护每个类加载器的已加载本地库列表。供应商应选择本地库名称,以最大程度地减少名称冲突的机会。
如果基础操作系统不支持动态链接,则必须将所有本地方法与VM预先链接。在这种情况下,VM无需实际加载库即可完成System.loadLibrary调用。
程序员还可以调用JNI函数RegisterNatives() 来注册与类关联的本地方法。 RegisterNatives()函数对于静态链接的函数特别有用。
Resolving Native Method Names
动态链接器根据名称来进行解析。本地方法名称由以下组件拼接而成:
VM检查与本地库中驻留的方法相匹配的方法名称。 VM首先寻找简称;即没有参数签名的名称。然后,它将查找长名称,即带有参数签名的名称。仅当本地方法被另一个本地方法重载时,程序员才需要使用长名称。无论如何,如果本地方法与java方法具有相同的名称,则这不是问题。非本机方法(Java方法)不驻留在本机库中。
在下面的示例中,不必使用长名称链接本驻留方法g,因为另一个方法g不是本地方法,因此不在本地库中。
class Cls1 {
int g(int i);
native int g(double d);
}
我们采用了一种简单的名称处理方案,以确保所有Unicode字符都转换为有效的C函数名称。 在完全限定的类名称中,我们使用下划线(_)代替斜杠(“ /”)。 由于名称或类型描述符从不以数字开头,因此我们可以将_0,...,_ 9用于转义序列,如表2-1所示:
Table 2-1 Unicode Character Translation
本地方法和接口API都在给定平台上遵循标准的库调用约定。 例如,UNIX系统使用C调用约定,而Win32系统使用__stdcall。
Native Method Arguments
JNI接口指针是本地方法的第一个参数。 JNI接口指针的类型为JNIEnv。 第二个参数根据本地方法是静态方法还是非静态方法而有所不同。 非静态本地方法的第二个参数是对对象的引用。 静态本地方法的第二个参数是对其Java类的引用。
其余参数对应于常规Java方法参数。 本地方法调用通过返回值将其结果传递回调用例程。 第3章介绍Java和C类型之间的映射。
Example 2-1演示了使用C函数来实现本地方法f。 本地方法f声明如下:
package pkg;
class Cls {
native double f(int i, String s);
...
}
长整齐的名称Java_pkg_Cls_f_ILjava_lang_String_2的C函数实现本地方法f:
Example 2-1 Implementing a Native Method Using C
jdouble Java_pkg_Cls_f__ILjava_lang_String_2 (
JNIEnv *env, /* interface pointer */
jobject obj, /* "this" pointer */
jint i, /* argument #1 */
jstring s) /* argument #2 */
{
/* Obtain a C-copy of the Java string */
const char *str = (*env)->GetStringUTFChars(env, s, 0);
/* process the string */
...
/* Now we are done with str */
(*env)->ReleaseStringUTFChars(env, s, str);
return ...
}
注意,我们总是使用接口指针env操作Java对象。 使用C ++,您可以编写稍微干净一点的代码版本,如Example 2-2所示:
Code Example 2-2 Implementing a Native Method Using C++
extern "C" /* specify the C calling convention */
jdouble Java_pkg_Cls_f__ILjava_lang_String_2 (
JNIEnv *env, /* interface pointer */
jobject obj, /* "this" pointer */
jint i, /* argument #1 */
jstring s) /* argument #2 */
{
const char *str = env->GetStringUTFChars(s, 0);
...
env->ReleaseStringUTFChars(s, str);
return ...
}
使用C ++时,额外的间接级别和接口指针参数将从源代码中消失。 但是,底层机制与C完全相同。在C ++中,JNI函数被定义为扩展为C对应函数的内联成员函数。
Referencing Java Objects
在Java和本地代码之间互相拷贝基本类型,例如整型,字符型等。 另一方面,Java对象通过引用传递。 VM必须追踪已传递给本地代码的所有对象,以使垃圾回收器不会回收这些对象。 反过来,本地代码不再需要java对象时,必须有方法来通知VM。 另外,垃圾收集器必须能够移除被本地代码引用的对象。
Global and Local References
JNI将本机代码使用的对象引用分为两类:局部引用和全局引用。 局部引用在本地方法调用期间有效,并在本地方法返回后自动释放。 全局引用在显式释放之前一直保持有效。
对象以局部引用的形式传递给本地方法。 JNI函数返回的所有Java对象都是局部引用。 JNI允许程序员从局部引用创建全局引用。 期望Java对象的JNI函数接受全局和局部引用。 本地方法可能会返回VM的局部或全局引用作为其结果。
在大多数情况下,程序员应在本地方法返回后依靠VM回收所有局部引用。 但是,有时程序员应该显式释放局部引用。 例如,考虑以下情况:
- 本地方法访问一个大的Java对象,并创建该Java对象的局部引用。 然后,本地方法将执行其他计算,然后再返回到调用方。 即使在其余的计算中不再使用该对象,该Java对象的局部引用也防止了GC对该对象进行垃圾回收。
- 本地方法创建了大量局部引用,尽管并非所有的局部引用都会被使用。 由于VM需要一定的空间来跟踪局部引用,因此创建太多局部引用可能会导致系统内存不足。 例如,本地方法遍历一个大容量的对象数组,检索元素作为局部引用,并在每次迭代时对一个元素进行操作。 每次迭代后,程序员不再需要这个数组元素的本地引用。
JNI允许程序员在本地方法中的任何时候手动删除局部引用。 为了确保程序员可以手动释放局部引用,不允许JNI函数创建额外的局部引用,除非它们会作为结果返回引用。
局部引用仅在创建它们的线程中有效。 本地代码不得将局部引用从一个线程传递到另一个线程。
Implementing Local References
为了实现局部引用,Java VM为从Java到本地方法的每次控制转换创建一个注册表。 注册表将不可删除的局部引用映射到Java对象,并防止垃圾回收对象。 传递给本地方法的所有Java对象(包括那些作为JNI函数调用结果返回的Java对象)都将自动添加到注册表中。 本地方法返回后,注册表将被删除,注册表中的所有子项都允许被GC垃圾回收。
有多种实现注册表的方法,例如使用表,链表或哈希表。 尽管可以使用引用计数来避免注册表中出现重复项,但是JNI实现没有义务检测并删除重复项。
请注意,不能通过保守地扫描本地堆栈来实现局部引用。 本地代码可以将局部引用存储到全局或堆数据结构中。
Accessing Java Objects
JNI为全局和局部引用提供了一组功能丰富的访问器。 无论VM在内部如何表示Java对象,都可以运行同一个本地方法。 这就是为什么JNI可以被各种VM支持的关键原因。
通过不透明引用使用访问器函数的开销比直接访问C数据结构的开销高。 我们认为,在大多数情况下,Java程序员使用本地方法来执行重要的任务,从而使该接口的开销变得微不足道。
Accessing Primitive Arrays
对于包含许多基本数据类型(例如整型数组和字符串)的大型Java对象,此开销是不可接受的。 (考虑用于执行矢量和矩阵计算的本地方法。)遍历Java数组并使用函数调用检索每个元素的效率很低。
一种解决方案引入了“固定”(pinning)的概念,以便本地方法可以要求VM固定数组的内容。 然后,本地方法接收指向元素的直接指针。 但是,此方法有两个含义:
- 垃圾收集器必须支持固定(pinning)。
- VM必须在内存中连续分配基本类型的数组。 尽管对于大多数基本类型的数组来说,这是最自然的实现,但是布尔数组可以打包或拆包形式实现。 因此,依赖于布尔数组的确切分配的本地代码将不可移植。
我们采取了可以解决上述两个问题的折衷方案。
首先,我们提供了一组函数,用于在一段Java数组和本地内存缓冲区之间复制基本数组元素。 如果本地方法仅需要访问大型数组中的少量元素,请使用这些函数。
其次,程序员可以使用另一组函数来获取固定形式的数组元素。 请记住,这些功能可能需要Java VM执行存储分配和复制。 实际上是否会复制数组取决于VM的实现,如下所示:
最后,当本地代码不再需要访问数组元素时,该接口提供了一些功能用于通知VM。 当您调用这些函数时,系统要么取消固定数组,要么协调原始数组与其固定的副本,然后释放该副本。
我们的方法提供了灵活性。 垃圾收集器算法可以为每个给定数组做出有关复制或固定的单独决策。 例如,垃圾收集器可以复制小对象,固定较大的对象。
JNI实现必须确保在多个线程中运行的本地方法可以同时访问同一数组。 例如,JNI可以为每个固定的数组保留一个内部计数器,以使一个线程不会取消固定被另一个线程固定的数组。 注意,JNI不需要锁定基本数组就可任意通过本地方法进行独占访问。 同时从不同的线程更新Java数组会导致不确定的结果。
Accessing Fields and Methods
The JNI allows native code to access the fields and to call the methods of Java objects. The JNI identifies methods and fields by their symbolic names and type signatures. A two-step process factors out the cost of locating the field or method from its name and signature. For example, to call the method f
in class cls, the native code first obtains a method ID, as follows:
JNI允许本地代码访问Java对象的字段并调用它的方法。 JNI通过它们的符号名和类型签名来标识方法和字段。从名称和签名中确定字段或方法分为两步。例如,要在类cls中调用方法f,本机代码首先获取方法ID,如下所示:
jmethodID mid = env->GetMethodID(cls, “f”, “(ILjava/lang/String;)D”);
然后,本地代码可以重复使用方法ID,而无需再次查找方法,如下所示:
jdouble result = env->CallDoubleMethod(obj, mid, 10, str);
字段ID或方法ID不会阻止VM将依据获取ID的类卸载。卸载类后,方法ID或字段ID就失效了。因此,如果打算长时间使用方法或字段ID,本机代码必须确保:
Reporting Programming Errors
JNI不检查编程错误,例如传入NULL指针或非法参数类型。 非法的参数类型包括诸如本来应该是Java类对象,却使用普通的Java对象。 由于以下原因,JNI不检查这些编程错误:
大多数C库函数都不能防止编程错误。 例如,当printf()函数接收到无效地址时,通常会导致运行时错误,而不是返回错误码。 强制C库函数检查所有可能的错误情况可能会导致重复这些检查(一次在用户代码中,然后在库中)。
程序员不得将非法的指针或错误类型的参数传递给JNI函数。 这样做可能会导致很严重的后果,包括损坏的系统状态或VM崩溃。
Java Exceptions
JNI允许本地方法引发任意Java异常。 本机代码还可以处理未解决的Java异常。 未处理的Java异常将传递回VM。
Exceptions and Error Codes
某些JNI函数使用Java异常机制来报告错误。 在大多数情况下,JNI函数通过返回错误码并引发Java异常来报告错误。 错误码通常是超出了正常返回值的范围的特殊的返回值(例如NULL)。 因此,程序员可以:
- 调用Java方法并返回Java方法的执行结果的JNI函数。 程序员必须调用ExceptionOccurred()来检查在执行Java方法期间可能发生的异常。
- 一些JNI数组访问函数不会返回错误码,但是可能会抛出ArrayIndexOutOfBoundsException或ArrayStoreException。
Asynchronous Exceptions(异步异常)
在有多线程的情况下,当前线程以外的其他线程可能会抛出异步异常。 异步异常不会立即影响当前线程中本地代码的执行,直到:
- 本地代码调用了一个可能引发同步异常的JNI函数,或者
- 本机代码使用ExceptionOccurred()显式检查同步和异步异常。
请注意,只有那些可能引发同步异常的JNI函数才会检查异步异常。
本机方法应在必要的位置插入ExceptionOccurred()检查(例如,在没有其他异常检查的循环调用中),以确保当前线程在合理的时间内响应异步异常。
Exception Handling
There are two ways to handle an exception in native code:
- The native method can choose to return immediately, causing the exception to be thrown in the Java code that initiated the native method call.
- The native code can clear the exception by calling
ExceptionClear()
, and then execute its own exception-handling code.
After an exception has been raised, the native code must first clear the exception before making other JNI calls. When there is a pending exception, the JNI functions that are safe to call are:
处理本地代码中的异常有两种方法:
- 本地方法可以选择立即返回,从而导致在调用本地方法的Java代码中抛出异常。
- 本地代码可以通过调用ExceptionClear()清除异常,然后执行其自己的异常处理代码。
引发异常后,本地代码必须首先清除异常,然后再进行其他JNI调用。 当存在未处理的异常时,以下的JNI函数是可以安全调用的:
ExceptionOccurred()
ExceptionDescribe()
ExceptionClear()
ExceptionCheck()
ReleaseStringChars()
ReleaseStringUTFChars()
ReleaseStringCritical()
Release<Type>ArrayElements()
ReleasePrimitiveArrayCritical()
DeleteLocalRef()
DeleteGlobalRef()
DeleteWeakGlobalRef()
MonitorExit()
PushLocalFrame()
PopLocalFrame()