JNI Chapter2:设计概述

原文连接

这一章主要介绍JNI的核心设计特点。这里所介绍的特点都是和native方法相关的。而Invocation API将在第5章介绍。

这章主要的内容:

  • JNI接口方法和指针
  • Loading and Linking Native Methods
    • 解析native方法名
    • native方法参数
  • 引用java对象
    • 全局引用和局部引用
    • 实现局部引用
  • 访问java对象
    • 访问基本类型数组
    • 访问 Fields and Methods
  • Java Exceptions
    • Exceptions and Error Codes
    • 异步Exceptions
    • 异常处理

JNI接口函数和指针

​ native代码通过JNI函数可以访问JVM的一些功能。JNI函数可以通过一个接口指针获取。该接口指针是一个二级指针(指向指针的指针)。指针指向了一个指针数组,指针数组中的每一个成员都指向一个接口函数。每一个接口函数都在数组的某个预定偏移量中。下图可以表明接口指针的组织关系。

JNI Chapter2:设计概述_第1张图片
JNI接口指针

​ JNI接口类似于C++的虚函数表或者COM的接口。与使用硬性编入的函数表相比,使用接口表的优势在于JNI的命名空间可以和native的代码分开。虚拟机可以很容易地提供多个版本的JNI函数表。例如,虚拟机可以支持如下两种JNI函数表:

  • 一种可以执行严格的非法参数检查并且适合调试
  • 另一种执行JNI规范文档中最低层次的参数检查,从而提高效率

​ JNI接口指针仅在执行线程中保持不变。因此,本地方法不可以从一个线程传递接口指针到另个线程。VM在实现JNI时应该分配并存储被JNI接口指针指向的线程数据。

​ 本地方法以参数形式获取JNI接口指针。当从同一个java线程中多次调运native方法时,虚拟机会确保传给这个native方法的接口指针是相同的。然而,一个native方法可以在不同的线程中被调运,因此可以接受不同的JNI接口指针。

Loading and Linking Native Methods

​ 本地方法由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本地库名。

​ 只要所有的java类由同一个classLoader对象加载,开发者就可以使用一个单独的库来存储任意数量的类所需要的本地方法。VM内部会为每一个classLoader维护一个本地库列表。VM构建者应该规范本地库的命名以减少名字冲突的几率。

​ 如果一个OS不支持动态链接,那么所有的本地方法都必须与VM链接。在这种情况下,虚拟机实际上不需要加载库即可完成System.loadLibrary调用。

​ 开发人员也可以调用JNI的RegisterNatives()函数来注册与一个类相关联的本地方法。RegisterNatives()函数在静态链接中相当有用。

解析native方法名

动态连接器根据native方法名来决定入口地址。一个native方法名由以下几个部分连结而成:

  • 前缀 Java_
  • 以下划线”_”分割开来的类名
  • 带有下划线”_”的方法名
  • 对于被加载的native方法,两个下划线紧随参数签名

VM会检查一个方法名是否匹配native库中的方法名。VM首先检查短名,也就是没有参数签名的名字。随后检查带有参数签名的长名。当一个native方法被另一个native方法重载,开发人员才需要使用长名。但是,native方法如果与非native方法(java方法)有一样的名字也是也可接受的,因为非native方法并不存在于native库中。

在接下来的示例中,本地方法g没有使用长名,因为另一个方法g是非本地方法不属于本地库。

class Cls1 {
    int g(int i);
    native int g(double d);
}

我们采用的简单命名方式可以确保所有的Unicode字符翻译为标准的C函数名。在完整的类名中,我们使用下划线"_"代替了斜线”/“。因为名字和类型描述符从不以数字开始,我们就是用_0,……,_9来表示转义字符。如下表

Escape Sequence Denotes
_0XXXX a Unicode character XXXX. Note that lower case is used to represent non-ASCII Unicode characters, for example, _0abcd as opposed to _0ABCD.
_1 the character “_”
_2 the character “;” in signatures
_3 the character “[“ in signatures

所有的本地方法和接口API都遵循平台的标准库调用规范。如,UNIX系统使用C调用规范,然而Win32系统使用_stdcall。

native方法参数

​ JNI接口指针是第一个进入本地方法的参数。JNI接口指针是JNIEnv类型的。第二个参数依据本地方法是静态还是非静态而不同。非静态本地方法的第二个参数一般是一个对象的引用。静态方法的第二个参数是所属java类的引用。

​ 余下的参数符合java方法的参数规律。本地方法调用使用返回值来传递结果给调用程序。Chapter 3中将描述java与C的类型关系。

​ 下面代码表明了使用 C 函数来实现本地方法f。本地方法f以如下形式声明

package pkg; 

class Cls {
    native double f(int i, String s);
    // ...
}

C函数以长命名方式Java_pkg_Cls_f_ILjava_lang_String_2的形式来实现本地方法f:

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++,可以实现结构更加清晰的一个版本,如代码所示:

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为蓝本的内联函数的形式定义。

引用java对象

​ 原始数据类型,如intergers,characters是在Java和native之间来回拷贝的,另一方面,任何的java对象的拷贝都是对引用的拷贝。vm必须记录所有被传递到native代码里的对象,以便这些对象不会被GC来回收。反过来,native必须有方法告诉VM,它不再需要某些对象了。另外,垃圾收集器也必须能够清理有native代码所引用的java对象。

全局引用和局部引用

​ JNI将引用对象划分成两类:局部和全局引用。局部引用在本地方法调用期间是可信的,并且在本地方法返回时自动被清理。全局引用在本地代码中一直存在,直至有了明确的清理要求。

​ 这里可以理解,在Java层新建对象分配内存,有GC来收集,而由本地方法新建的对象,则无法由GC来管理,因此必须自己来管理它们。

​ 而对于本地引用,有点类似Java栈里面的局部变量,进入方法时栈帧里分配内存,退出方法栈帧的时候自动释放内存,因此这部分的内存其实是不需要GC来管理。

​ 而对于全部引用,应该遵循谁分配谁释放的约定,由本地方法分配内存,并由本地方法释放内存。

​ 对象以局部引用的形式传递到本地方法中。所有本JNI函数返回的java对象都是局部引用形式。JNI允许开发着将局部引用转变为全局引用。JNI函数允许java对象是局部引用和全局引用。本地方法可以以局部引用或全局引用的形式对VM返回结果。

​ 在大多数情况下,开发人员应该依赖VM来清理所有的局部引用。但是,有几个特殊情况需要开发人员明确地清理局部引用。如下面情形:

  • 一个本地方法访问一个超大的Java对象,因此会为这个Java对象创建一个局部引用。本地方法还需要在返回之前做一些其他计算,而这种因为有一个局部引用指向这个Java对象,因此垃圾回收器不能回收掉这个大的对象。而这个对象已经没有用了,因此需要本地方法明确的去释放它,来使得这个java对象可以被释放掉。
  • 一个本地方法创建了大量的局部引用,但不是同时都需要它们。因此虚拟机需要大量空间来持续跟踪这些局部引用,所以创建大量的局部引用可能会造成系统内部不足。例如一个本地放在在for循环一个大的数组,数组里的元素以局部引用的形式引用,在遍历的时候一次操作一个元素,而在遍历结束以后,开发者已经不再需要这个数组里面的元素的局部引用了。

​ JNI允许开发人员在本地方法的任意位置主动地清理局部引用。为了确保开发人员可以清理局部引用,JNI函数不会再创建额外的局部引用,除了作为返回值的引用。

​ 局部引用只会存在本地方法所在的线程中。native代码不可以将一个引用从一个线程传递到另一个线程。

实现局部引用

​ 为了实现局部引用,JVM为每一个从java到native方法的过渡控制建立以注册表。每一个注册表都映射了局部映射到java对象,防止该对象被垃圾收集器回收。所有传递到native方法的java对象(包括那些作为JNI函数调用返回值的java对象)都自动地添加到注册表中。当native方法返回值后,注册表就会被删除并且所有的入口点都会被垃圾收集器回收。

​ 有多种方式可以实现注册表,如使用table,linked list或者hash table。尽管为避免重复的入口点会在注册表中使用引用计数,但JNI的实现中并不总是检查并清理重复的入口点。

注意:局部引用不会忠实地通过扫描本地栈结构来实现。native代码也可能会将局部引用存储到全局或堆结构中。

访问java对象

​ JNI在全局和局部引用中提供了一组丰富的读值函数(accessor functions)。这意味着同样的native方法实现在任何VM(不管VM内部是如何表达java对象的)中都是奏效的。这也是JNI可以被大量的VM支持的重要原因。

​ 通过不透明的引用来使用访问函数的开销比直接访问 C 数据结构的开销来得高。我们相信,大多数情况下,Java 程序员使用本地方法是为了完成一些重要任务,此时这种接口的开销不是首要问题。

访问基本类型数组

​ 对于含有大量基本数据类型(如整数数组和字符串)的 Java 对象来说,这种开销将高得不可接受 (考虑一下用于执行矢量和矩阵运算的本地方法的情形便知)。对 Java 数组进行迭代并且要通过函数调用取回数组的每个元素,其效率是非常低的。

​ 一个解决办法是引入“钉住”概念,以使本地方法能够要求虚拟机钉住数组内容。而后,该本地方法将接受指向数值元素的直接指针。但是,这种方法包含以下两个前提:

  • 垃圾收集器必须支持钉住。

  • 虚拟机必须在内存中连续存放基本类型数组。虽然大多数基本类型数组都是连续存放的,但布尔数组可以压缩或不压缩存储。因此,依赖于布尔数组确切存储方式的本地方法将是不可移植的。

    ​我们将采取折衷方法来克服上述两个问题。

    ​首先,我们提供了一套函数,用于在 Java 数组的一部分和本地内存缓冲之间复制基本类型数组元素。这些函数只有在本地方法只需访问大型数组中的一小部分元素时才使用。

其次,程序员可用另一套函数来取回数组元素的受约束版本。记住,这些函数可能要求 Java 虚拟机分配存储空间和进行复制。虚拟机实现将决定这些函数是否真正复制该数组,如下所示:

  • 如果垃圾收集器支持钉住,且数组的布局符合本地方法的要求,则不需要进行复制。
  • 否则,该数组将被复制到不可移动的内存块中(例如,复制到 C 堆中),并进行必要的格式转换,然后返回指向该副本的指针。

​最后,接口提供了一些函数,用以通知虚拟机本地方法已不再需要访问这些数组元素。当调用这些函数时,系统或者释放数组,或者在原始数组与其不可移动副本之间进行协调并将副本释放。

​这种处理方法具有灵活性。垃圾收集器的算法可对每个给定的数组分别作出复制或钉住的决定。例如,垃圾收集器可能复制小型对象而钉住大型对象。

​JNI 实现必须确保多个线程中运行的本地方法可同时访问同一数组。例如,JNI 可以为每个被钉住的数组保留一个内部计数器,以便某个线程不会解开同时被另一个线程钉住的数组。注意,JNI 不必将基本类型数组锁住以专供某个本地方法访问。同时从不同的线程对 Java 数组进行更新将导致不确定的结果。

访问字段和方法

​ 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 并不能防止虚拟机卸载生成该 ID 的类。该类被卸载之后,该方法 ID 或字段 ID 亦变成无效。因此,如果平台相关代码要长时间使用某个方法 ID 或字段 ID,则它必须确保:

  • 保留对所涉及类的活引用
  • 重新计算该方法 ID 或字段 ID

JNI 对字段 ID 和方法 ID 的内部实现并不施加任何限制。

Reporting Programming Errors

​ JNI 不检查诸如传递 NULL 指针或非法参数类型之类的编程错误。非法的参数类型包括诸如要用 Java 类对象时却用了普通 Java 对象这样的错误。JNI 不检查这些编程错误的理由如下:

  • 强迫 JNI 函数去检查所有可能的错误情况将降低正常(正确)的本地方法的性能。(影响性能)
  • 在许多情况下,没有足够的运行时的类型信息可供这种检查使用。(大多数情况下,信息不全)

​大多数 C 库函数对编程错误不进行防范。例如,printf() 函数在接到一个无效地址时通常是引起运行错而不是返回错误代码。强迫 C 库函数检查所有可能的错误情况将有可能引起这种检查被重复进行--先是在用户代码中进行,然后又在库函数中再次进行。

​程序员不能将非法指针或错误类型的参数传递给 JNI 函数。否则,可能产生意想不到的后果,包括可能使系统状态受损或使虚拟机崩溃。

java异常

​ JNI 允许本地方法抛出任何 Java 异常。本地方法也可以处理突出的 Java 异常。未被处理的 Java 异常将被传回虚拟机中。

异常和错误码

​ 一些 JNI 函数使用 Java 异常机制来报告错误情况。大多数情况下,JNI 函数通过返回错误代码并抛出 Java 异常来报告错误情况。错误代码通常是特殊的返回值(如 NULL),这种特殊的返回值在正常返回值范围之外。因此,程序员可以:

  • 快速检查上一个 JNI 调用所返回的值以确定是否出错
  • 通过调用函数 ExceptionOccurred()来获得异常对象,它含有对错误情况的更详细说明。

在以下两种情况中,程序员需要先查出异常,然后才能检查错误代码:

  • 调用 Java 方法的 JNI 函数返回该 Java 方法的结果。程序员必须调用 ExceptionOccurred() 以检查在执行 Java 方法期间可能发生的异常。
  • 某些用于访问 JNI 数组的函数并不返回错误代码,但可能会抛出 ArrayIndexOutOfBoundsExceptionArrayStoreException

在所有其它情况下,返回值如果不是错误代码值就可确保没有抛出异常。

异步异常

在多个线程的情况下,当前线程以外的其它线程可能会抛出异步异常。异步异常并不立即影响当前线程中平台相关代码的执行,直到出现下列情况:

  • native代码调用某个有可能抛出同步异常的 JNI 函数
  • native代码用 ExceptionOccurred() 显式检查同步异常或异步异常。

注意,只有那些有可能抛出同步异常的 JNI 函数才检查异步异常。

本地方法应在必要的地方(例如,在一个没有其它异常检查的紧密循环中)插入 ExceptionOccurred() 检查以确保当前线程可在适当时间内对异步异常作出响应。

异常处理

有两种方法来处理native相关代码中的异常:

  • 本地方法可选择立即返回,使异常在启动该本地方法调用的 Java 代码中抛出。
  • 平台相关代码可通过调用 ExceptionClear() 来清除异常,然后执行自己的异常处理代码。

抛出了某个异常之后,平台相关代码必须先清除异常,然后才能进行其它的 JNI 调用。当有待定异常时,只有以下这些 JNI 函数可被安全地调用:

ExceptionOccurred()
ExceptionDescribe()
ExceptionClear()
ExceptionCheck()
ReleaseStringChars()
ReleaseStringUTFChars()
ReleaseStringCritical()
ReleaseArrayElements()
ReleasePrimitiveArrayCritical()
DeleteLocalRef()
DeleteGlobalRef()
DeleteWeakGlobalRef()
MonitorExit()
PushLocalFrame()
PopLocalFrame()

你可能感兴趣的:(JNI Chapter2:设计概述)