Jimmy Chen

A Programmer

(译文) JNI编程指南与规范 第十一章 JNI设计概述

第十一章 JNI设计概述

本章给出了JNI设计的概述,必要时,我们也提供底层技术的动机。设计概述作为关键JNI概念(如JNIEnv接口指针,本地和全局引用以及字段和方法ID)的规范。技术动机旨在帮助读者了解各种设计的权衡。有几次,我们将讨论如何实现某些功能。这种讨论的目的不是提出一个实际的实施策略,而是要澄清微妙的语义问题。

  桥接不同语言的编程接口的概念并不新鲜。例如,C程序通常可以调用用FORTRAN和汇编语言编写的函数。同样,编程语言(如LISP和Smalltalk)的实现也支持各种外部函数接口。

  JNI解决了类似于由其他语言支持的互操作性机制所解决的问题。然而,JNI与许多其他语言中使用的互操作机制之间存在显着差异。JNI不是为Java虚拟机的特定实现而设计的。相反,它是每个Java虚拟机实现都可以支持的本地接口。我们将在描述JNI设计目标时进一步阐述这一点。

11.1 设计目标

JNI设计的最重要目标是确保它在给定的主机环境中提供不同Java虚拟机实现之间的二进制兼容性。相同的本地库二进制文件将运行在给定主机环境的不同虚拟机实现上,而不需要重新编译。

  为了实现这个目标,JNI设计不能对Java虚拟机实现的内部细节做任何假设。由于Java虚拟机实现技术正在迅速发展,所以我们必须小心,以避免引入任何可能会影响未来的高级实现技术的限制。

  JNI设计的第二个目标是效率。为了支持对时间要求严格的代码,JNI强加了尽可能少的开销。但是,我们将看到,我们的第一个目标,即实现独立的需要,有时需要我们采用比我们其他方式效率稍低的设计。我们在效率和实现独立性之间达成妥协。

  最后,JNI必须功能完整。 它必须公开足够多的Java虚拟机功能,以使本地方法和应用程序能够完成有用的任务。

  JNI的目标不是成为给定的Java虚拟机实现所支持的唯一的本地编程接口。一个标准接口有利于程序员,他们希望将他们的本地代码库加载到不同的Java虚拟机实现中。但是,在某些情况下,较低级别的特定于实现的接口可能会获得更高的性能。在其他情况下,程序员可能使用更高级别的接口来构建软件组件。

11.2 加载本地库

在应用程序可以调用本地方法之前,虚拟机必须定位并加载包含本地方法实现的本地库。

11.2.1 类加载器

本地库位于类加载器中。 类加载器在Java虚拟机中有许多用途,例如加载类文件,定义类和接口,在软件组件之间提供命名空间分离,解决不同类和接口间的符号引用,最后定位本地库。 我们假设您对类加载器有基本的了解,所以我们不会详细介绍如何在Java虚拟机中加载和链接类。 关于类加载器的更多详细信息,你可以参阅由Sheng Liang和Gilad Bracha在ACM面向对象编程系统,语言和应用程序会议(OOPSLA)的会议记录中的记录的《Dynamic Class Loading in the Java Virtual Machine》。

  类加载器提供了在同一虚拟机的一个实例中运行多个组件(比如从不同网站下载的applet)所需的命名空间分离技术。类加载器通过在Java虚拟机中将类或接口的名称映射到对象的实际类或接口类型来维护单独的名称空间。每个类或接口类型都与其定义的加载程序相关联,该加载程序最初读取类文件并定义类或接口对象。只有两个类或接口类型具有相同的名称和相同的定义加载器时,才是相同的。 例如,在图11.1中,类加载器L1和L2都定义了一个名为C的类。这两个名为C的类是不一样的。确实,它们包含两种不同的f方法,它们具有不同的返回类型。

《(译文) JNI编程指南与规范 第十一章 JNI设计概述》

  上图中的虚线表示类加载器之间的委托关系。一个类加载器可能要求另一个类加载器代表它加载一个类或一个接口。例如,L1和L2都委托给引导类加载器来加载系统类java.lang.String。委托允许系统类在所有类加载器之间共享。这是必要的,因为例如,如果应用程序和系统代码对java.lang.String类型的内容有不同的概念,则会违反类型安全性。

11.2.2 类加载器和本地库

现在假设两个类C中的方法f都是本地方法。 虚拟机使用名称“C_f”定位两个C.f方法的本机实现。 为了确保每个C类与正确的本地函数链接,每个类加载器都必须维护自己的一组本机库,如图11.2所示。

《(译文) JNI编程指南与规范 第十一章 JNI设计概述》

  因为每个类加载器都维护着一组本地库,所以只要这些类具有相同的定义加载器,程序员可以使用一个库来存储任意数量的类所需的所有本地方法。

  当相应的类加载器被垃圾收集时,本地库会自动被虚拟机卸载(11.2.5节)。

11.2.3 定位本地库

本地库由System.loadLibrary方法加载。 在以下示例中,类Cls的静态初始化程序会加载一个特定于平台的本机库,其中定义了本机方法f:

package pkg; 
class Cls {
    native double f(int i, String s); 
    static {
        System.loadLibrary("mypkg"); 
    } 
}

  System.loadLibrary的参数是程序员选择的库名。软件开发人员负责选择本地库名称,以最大限度地减少名称冲突的机会。虚拟机遵循一个标准的,但主机环境特定的约定将库名称转换为本地库名称。例如,Solaris操作系统将名称mypkg转换为libmypkg.so,而Win32操作系统将相同的mypkg名称转换为mypkg.dll。

  当Java虚拟机启动时,它会构建一个目录列表,这些目录将用于定位应用程序中类的本地库。列表的内容取决于主机环境和虚拟机的实现。例如,在Win32 JDK或Java 2 SDK版本下,目录列表由Windows系统目录,当前工作目录以及PATH环境变量中的条目组成。在Solaris JDK或Java 2 SDK发行版中,目录列表由LD_LIBRARY_PATH环境变量中的条目组成。

  如果System.loadLibrary无法加载指定的本机库,则会抛出UnsatisfiedLinkError。如果之前对System.loadLibrary的调用已经加载了相同的本地库,System.loadLibrary将自动完成。如果底层操作系统不支持动态链接,则必须将所有本机方法预先链接到虚拟机。在这种情况下,虚拟机完成System.loadLibrary调用而不实际加载库。

  虚拟机在内部为每个类加载器维护一个加载的本地库列表。它遵循三个步骤来确定哪个类加载器应该与新加载的本地库相关联:

  1. 确定System.loadLibrary的直接调用者
  2. 标识定义调用者的类
  3. 获取调用者类的定义加载器

在以下示例中,本机库foo将与C的定义加载器相关联:

  Java 2 SDK版本1.2引入了新的ClassLoader.findLibrary方法,允许程序员指定特定于给定类加载器的自定义库加载策略。ClassLoader.findLibrary方法将平台无关的库名称(如mypkg)作为参数,并且:

  • 要么返回空指示虚拟机遵循默认的库搜索路径
  • 或返回库文件的主机环境相关的绝对路径(如“c:\\mylibs\\mypkg.dll”)。

  ClassLoader.findLibrary通常与Java 2 SDK版本1.2中添加的另一个方法System.mapLibraryName一起使用。System.mapLibraryName将独立于平台的库名(例如mypkg)映射到平台相关的库文件名(例如mypkg.dll)。

  你可以通过设置属性java.library.path来覆盖Java 2 SDK版本1.2中的默认库搜索路径。例如,下面的命令行启动一个程序Foo,它需要在c:\mylibs目录中加载一个本地库:

java -Djava.library.path=c:\mylibs Foo

11.2.4 类型安全限制

虚拟机不允许给定的JNI本地库由多个类加载器加载。尝试通过多个类加载器加载相同的本机库会导致抛出UnsatisfiedLinkError异常。此限制的目的是确保基于类加载器的名称空间分离机制保留在本地库中。没有这个限制,通过本地方法错误地混合来自不同类加载器的类和接口将会变得更容易。考虑一个本地方法Foo.f,它在全局引用中将自己的定义类Foo进行缓存:

JNIEXPORT void JNICALL
Java_Foo_f(JNIEnv *env, jobject self) {
    static jclass cachedFooClass; /* cached class Foo */ 
    if (cachedFooClass == NULL) {
        jclass fooClass = (*env)->FindClass(env, "Foo"); 
        if (fooClass == NULL) { 
            return; /* error */
        }
        cachedFooClass = (*env)->NewGlobalRef(env, fooClass); 
        if (cachedFooClass == NULL) { 
            return; /* error */
        } 
    }
    assert((*env)->IsInstanceOf(env, self, cachedFooClass)); ... /* use cachedFooClass */
}

  我们期望断言成功,因为Foo.f是一个实例方法,而self是指Foo的一个实例。但是,如果两个不同的Foo类由类加载器L1和L2加载,并且两个Foo类都与前一个Foo.f实现链接,则断言可能会失败。cachedFooClass全局引用将为首先调用f方法的Foo类创建。稍后调用另一个Foo类的f方法将导致断言失败。

  JDK版本1.1没有正确执行类加载器之间的本地库分离。这意味着不同类加载器中的两个类可以使用相同的本地方法进行链接。如前面的例子所示,JDK版本1.1中的方法导致了以下两个问题:

  • 一个类可能会错误地链接到在不同类加载器中由具有相同名称的类加载的本地库。
  • 本地方法可以轻松地混合来自不同类加载器的类。这打破了类加载器提供的名称空间分离,并导致类型安全问题。

11.2.5 卸载本地库

在垃圾收集与本地库相关联的类加载器之后,虚拟机将卸载本机库。因为类引用了它们定义的加载器,这意味着虚拟机也卸载了在其静态初始化器中调用System.loadLibrary并加载了本地库(11.2.2节)的类。

11.3 链接本地方法

虚拟机在第一次调用本地方法前,它会尝试去链接每一本地方法。本地方法f可以被链接的最早时间是方法g的第一次调用,其中g的方法体有对f的引用。虚拟机实现不应该尝试过早地链接本地方法。 这样做可能会导致意外的链接错误,因为实现本地方法的本地库可能尚未加载。

  链接本地方法涉及以下步骤:

  • 确定定义本地方法的类的类加载器。
  • 搜索与这个类加载器相关联的本地库集,来定位实现这个本地方法的本地函数。
  • 设置内部数据结构,以便今后所有对本地方法的调用将直接跳转到本地函数。

  虚拟机通过连接以下组件来从本机方法的名称中推导出本机函数的名称:

  • 前缀“java_”
  • 一个编码的全类名
  • 一个下划线分隔符
  • 一个编码的函数名
  • 对于重载的本地方法,两个下划线(“__”)后跟编码的参数描述符

  虚拟机遍历与定义的加载器相关联的所有本机库,以适当的名称搜索本机函数。于每个本地库,虚拟机首先查找短名称,即不带参数描述符的名称。 然后它查找长名称,这是带有参数描述符的名称。 只有当本地方法被另一个本地方法重载时,程序员才需要使用长名称。 但是,如果使用非本地方法重载本机方法,则这不是问题。 后者不在本地库中。

  在以下示例中,本机方法g不必使用长名称进行链接,因为其他方法g不是本机方法。

class Cls1 {
    int g(int i) { ... } // regular method 
    native int g(double d);
}

  JNI采用简单的名称编码方案,以确保所有的Unicode字符都转换为有效的C函数名称。下划线(“_”)字符分隔完全格式的类名的组件。由于名称或类型描述符从不以数字开头,所以我们可以使用_0,…,_9作为转义序列,如下所示:

《(译文) JNI编程指南与规范 第十一章 JNI设计概述》

  如果存在与多个本地库中编码的本机方法名称匹配的本机函数,则首先加载的本机库中的函数与本机方法链接。 如果没有函数匹配本地方法名称,则抛出UnsatisfiedLinkError。

  程序员也可以调用JNI函数RegisterNatives来注册与类关联的本地方法。 RegisterNatives函数在静态链接函数中特别有用。

11.4 调用规则

调用约定确定本机函数如何接收参数并返回结果。 在各种本地语言之间或同一种语言的不同实现之间没有标准的调用约定。 例如,不同的C ++编译器生成遵循不同调用约定的代码是很常见的。

  如果不是不可能的话,要求Java虚拟机与各种本地调用约定进行互操作将是困难的。JNI要求在一给定的主机环境中使用特定的标准调用规则来编写本地方法就。例如,JNI遵循UNIX上的C调用约定和Win32上的stdcall约定。

  当程序员需要调用遵循不同调用约定的函数时,他们必须编写将JNI调用约定调整为适合本地语言的调用约定。

11.5 JNIEnv接口指针

本地代码通过调用通过JNIEnv接口指针导出的各种函数来访问虚拟机功能。

11.5.1 JNIEnv接口指针的组织

一个JNIEnv接口指针是一个指向本地线程数据的指针,每个接口函数都在表中的预定义偏移处。NIEnv接口像C ++虚拟函数表一样组织,也像Microsoft COM接口。 图11.3展示了一组JNIEnv接口指针。

《(译文) JNI编程指南与规范 第十一章 JNI设计概述》

  实现本地方法的函数接收JNIEnv接口指针作为它们的第一个参数。虚拟机保证将相同的接口指针传递给从同一线程调用的本地方法实现函数。但是,可以从不同的线程调用本地方法,因此可以传递不同的JNIEnv接口指针。虽然接口指针是线程本地的,但双重间接JNI函数表是由多个线程共享的。

  JNIEnv接口指针引用线程本地结构的原因是有些平台没有高效的线程本地存储访问支持。通过传递一个线程本地指针,虚拟机内部的JNI实现可以避免许多线程本地存储访问操作,否则它将不得不执行。

  因为JNIEnv接口指针是线程本地的,所以本地代码不能使用属于另一个线程中的一个线程的JNIEnv接口指针。本地代码可能会使用JNIEnv指针作为线程ID,在线程的生命周期中保持唯一。

11.5.2 接口指针的好处

与硬连线函数条目相反.使用接口指针有几个优点:

  • 最重要的是,因为JNI函数表作为参数传递给每个本地方法,所以本地库不必与Java虚拟机的特定实现链接。 这是至关重要的,因为不同的供应商可能会以不同的方式命名虚拟机实现。在一个给定的主机环境中,然后每个本地库自成一体是让相同的本地库能够在不同厂商之间的虚拟机工作的先决条件。
  • 其次,通过不使用硬连线功能条目,虚拟机实现可以选择提供多个版本的JNI功能表。例如,虚拟机实现可能支持两个JNI函数表:一个执行彻底的非法参数检查,适合调试;另一个执行JNI规范所要求的最小量的检查,因此更高效。Java 2 SDK版本1.2支持-Xcheck:jni选项,可以选择打开对JNI函数的额外检查。
  • 最后,多个JNI函数表使将来能够支持多个类似JNIEnv的接口成为可能。虽然我们还没有预见到需要这样做,但除了1.1和1.2版本中JNIEnv接口所指向的版本之外,未来版本的Java平台还可以支持新的JNI函数表。Java 2 SDK版本1.2引入了一个JNI_Onload函数,该函数可以由本地库定义,以指示本地库所需的JNI函数表的版本。Java虚拟机的未来实现可以同时支持多个版本的JNI函数表,并根据需要将正确的版本传递给各个本地库。

11.6 传递数据

在Java虚拟机和本地代码之间能够复制原始数据类型,例如整数,字符等。另一方面,对象通过引用传递。每个引用都包含一个指向底层对象的直接指针。本地代码从不直接使用指向该对象的指针。从本地代码的角度来看,引用是不透明的。

  传递引用而不是直接指向对象的指针可以使虚拟机以更灵活的方式管理对象。 图11.4说明了这种灵活性。 当本地代码持有引用时,虚拟机可能会执行垃圾回收,导致将对象从一个内存区域复制到另一个区域。 虚拟机可以自动更新引用的内容,以便虽然对象已经移动,但引用仍然有效。

《(译文) JNI编程指南与规范 第十一章 JNI设计概述》

11.6.1 全局和本地引用

JNI为本地代码创建两种对象引用:本地和全局引用。本地引用在本地方法调用期间有效,并在本地方法返回后自动释放。全局引用在显式释放前保持有效。

  对象作为本地引用传递给本地方法。大多数JNI函数返回本地引用。JNI允许程序员从本地引用创建全局引用。将对象作为参数的JNI函数接受全局和本地引用。作为结果,本地方法可能会返回本地或全局引用到虚拟机。

  本地引用仅在创建它们的线程中有效。本地代码不得将本地引用从一个线程传递到另一个线程。

  JNI中的NULL引用是指Java虚拟机中的空对象。其值不为NULL的本地或全局引用不引用空对象。

11.6.2 实现本地引用

958/5000
为了实现本地引用,Java虚拟机为每个从虚拟机到本地方法的控制转换创建一个注册表。 注册表将不可移动的本地引用映射到对象指针。 注册中的对象不能被垃圾收集。 传递给本地方法的所有对象,包括那些作为JNI函数调用结果返回的对象,都会自动添加到注册表中。 本地方法返回后,注册表被删除,允许其条目被垃圾收集。 图11.5说明了如何创建和删除本地引用注册表。 对应于本地方法的Java虚拟机框架包含指向本地引用注册表的指针。 D.f方法调用本地方法C.g. C.g由C函数Java_C_g实现。 虚拟机在输入Java_C_g之前创建本地参考注册表,并在Java_C_g返回后删除本地参考注册表。

《(译文) JNI编程指南与规范 第十一章 JNI设计概述》

  有不同的方法来实现注册表,例如使用堆栈,表,链表或哈希表。尽管可以使用引用计数来避免注册表中的重复条目,但JNI实现并不一定要检测和折叠重复的条目。

  本地引用不能忠实地通过保守地扫描本地堆栈来实现。本机代码可能会将本地引用存储到全局或C堆数据结构中。

11.6.3 弱全局引用

Java 2 SDK版本1.2引入了一种新的全局引用:弱全局引用。 与普通的全局引用不同,弱全局引用允许引用的对象被垃圾收集。 在底层对象被垃圾收集之后,清除弱全局引用。 本地代码可以通过使用IsSameObject来比较引用与NULL来测试弱全局引用是否被清除。

11.7 访问对象

JNI为引用对象提供了丰富的访问函数。 这意味着无论虚拟机如何在内部表示对象,相同的本地方法实现都可以工作。 这是一个至关重要的设计决策,使JNI能够被任何虚拟机实现所支持。

  通过不透明引用使用访问函数的开销高于直接访问C数据结构的开销。我们认为,在大多数情况下,本地方法执行不重要的任务,从而掩盖额外函数调用的成本。

11.7.1 访问基本数据类型数组

但是,函数调用的开销是不可接受的,因为重复访问大对象(如整型数组和字符串)中的基元数据类型的值。考虑用于执行向量和矩阵计算的本机方法。遍历一个整数数组并遍历一个函数调用将是非常低效的。

  一种解决方案引入了“钉住”的概念,以便本地方法可以要求虚拟机不要移动数组的内容。本地方法然后接收到元素的直接指针。但是,这种方法有两个含义:

  • 垃圾回收器必须支持钉住。在许多实现中,钉住是不合需要的,因为它会使垃圾收集算法复杂化并导致内存碎片化。
  • 虚拟机必须在内存中连续放置原始数组。虽然这是大多数基本数组的自然实现,但布尔数组可以实现为压缩或解压缩。一个打包的布尔数组为每个元素使用一个位,而一个解压缩的数组通常为每个元素使用一个字节。因此,依赖布尔数组的确切布局的本机代码将不可移植。

  JNI采取了解决上述两个问题的妥协方案。

  首先,JNI提供了一组函数(例如,GetIntArrayRegion和SetIntArrayRegion)来复制原始数组的片段与原生内存缓冲区之间的原始数组元素。如果本地方法只需要访问大数组中的少量元素,或者本地方法需要复制数组,则可以使用这些函数。

  其次,程序员可以使用另一组函数(例如,GetIntArrayElements)尝试获取固定版本的数组元素。但是,根据虚拟机的实现情况,这些功能可能会导致存储分配和复制。这些函数实际上是否复制数组取决于虚拟机的实现,如下所示:

  • 如果垃圾收集器支持固定,并且数组的布局与相同类型的本地数组的布局相同,则不需要复制。
  • 否则,将该数组复制到一个不可移动的内存块(例如,C堆)中,并执行必要的格式转换。返回指向该副本的指针。

  本地代码调用第三组函数(例如,ReleaseIntArrayElements)来通知虚拟机本地代码不再需要访问数组元素。当发生这种情况时,虚拟机要么取消数组,要么使原始数组与其不可移动的副本一致,并释放副本。

  该方法提供了灵活性。垃圾收集器算法可以针对每个数组做出复制或固定的独立决定。在特定的实现方案下,垃圾收集器可能会复制小数组,但会插入大数组。

  最后,Java 2 SDK版本1.2引入了两个新的函数:etPrimitiveArrayCritical和ReleasePrimitiveArrayCritical。这些函数可以以类似于GetIntArrayElements和ReleaseIntArrayElements的方式使用。但是,在使用GetPrimitiveArrayCritical获取指向数组元素的指针之后,在使用ReleasePrimitiveArrayCritical释放指针之前,本地代码有很大的限制。在“关键区域”内,本机代码不应该无限期地运行,不能随意调用JNI函数,也不能执行可能导致当前线程阻塞并等待虚拟机中的另一个线程的操作。鉴于这些限制,虚拟机可以临时禁用垃圾回收,同时让本地代码直接访问数组元素。因为不需要固定支持,所以GetPrimitiveArrayCritical更有可能返回一个直接指向原始数组元素的指针,比如GetIntArrayElements。

  JNI实现必须确保在多个线程中运行的本地方法可以同时访问相同的数组。 例如,JNI可能会为每个固定数组保留一个内部计数器,以便一个线程不会取消固定另一个线程固定的数组。 请注意,JNI不需要锁定原始数组以独占本地方法。 同时允许从不同线程更新数组,虽然这会导致不确定的结果。

11.7.2 字段和方法

JNI允许本地代码访问字段并调用Java编程语言中定义的方法。JNI通过它们的符号名称和类型描述符来标识方法和字段。两个步骤的过程将是从其名称和描述符中找出字段或方法的成本。例如,要读取类cls中的整数实例字段i,本机代码首先获取一个字段ID,如下所示:

jfieldID fid = env->GetFieldID(env, cls, "i", "I");

然后,本机代码可以重复使用字段ID,而不需要字段查找的代价,如下所示:

jint value = env->GetIntField(env, obj, fid);

  字段或方法ID保持有效,直到虚拟机卸载定义相应字段或方法的类或接口。类或接口卸载后,方法或字段ID将变为无效。

  程序员可以从解析相应字段或方法的类或接口派生一个字段或方法ID。字段或方法可以在类或接口本身中定义,也可以从超类或超接口继承。Java™虚拟机规范包含解析字段和方法的精确规则。如果从这两个类或接口中解析相同的字段或方法定义,则JNI实现必须从两个类或接口派生相同的字段或方法ID。例如,如果B定义了字段fld,并且C从B继承了fld,那么程序员保证从类B和C中获得字段名“fld”的相同字段ID。

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

  请注意,你需要字段名称和字段描述符以从给定的类或接口获取字段ID。这看起来没有必要,因为字段不能用Java编程语言重载。但是,在类文件中重载字段并在Java虚拟机上运行这样的类文件是合法的。因此,JNI能够处理由Java编程语言的编译器不生成的合法类文件。

  只有程序员知道方法或字段的名称和类型,才能使用JNI调用方法或访问字段。相比之下,Java核心反射API允许程序员确定给定类或接口中的字段和方法集合。在本地代码中反射类或接口类型有时也是有用的。Java 2 SDK版本1.2提供了新的JNI函数,可以与现有的Java核心反射API配合使用。新函数包括一对在JNI字段ID和java.lang.reflect.Field类的实例之间转换,另一对在JNI方法ID和java.lang.reflect.Method类的实例之间转换。

11.8 错误和异常

JNI编程中的错误与Java虚拟机实现中发生的错误不同。程序员错误是由于滥用JNI函数引起的。例如,程序员可能会错误地将对象引用而不是类引用传递给GetFieldID。引发Java虚拟机异常,例如,当本机代码试图通过JNI分配一个对象时,出现内存不足的情况。

11.8.1 没有检查编程错误

JNI函数不检查编程错误。将非法参数传递给JNI函数会导致未定义的行为。这个设计决定的原因如下:

  • 强制JNI函数检查所有可能的错误条件会降低所有(通常是正确的)本地方法的性能。
  • 在许多情况下没有足够的运行时类型信息来执行这种检查。

  大多数C库函数不防范编程错误。例如,printf函数通常会触发运行时错误,而不是在收到无效地址时返回错误代码。强制C库函数检查所有可能的错误条件可能会导致这种检查被重复,一次在用户代码中,然后再次在库中。

  尽管JNI规范不要求虚拟机检查编程错误,但是鼓励虚拟机实现检查常见的错误。 例如,虚拟机可以在JNI函数表(第11.5.2节)的调试版本中执行更多的检查。

11.8.2 Java虚拟机异常

JNI不依赖于本地编程语言中的异常处理机制。原生代码可能会导致Java虚拟机通过调用Throw或ThrowNew引发异常。在当前线程中记录未决异常。与Java编程语言中抛出的异常不同,本机代码抛出的异常不会立即中断当前的执行。

  本地语言没有标准的异常处理机制。因此,JNI程序员需要在每个可能会抛出异常的操作之后检查并处理异常。JNI程序员可以通过两种方式处理异常:

  • 本地方法可能会选择立即返回,导致在启动本机方法调用的代码中引发异常。
  • 本地代码可以通过调用ExceptionClear清除异常,然后执行自己的异常处理代码。

  在调用任何后续的JNI函数之前,检查,处理和清除待处理的异常是非常重要的。调用带有未决异常的大多数JNI函数会导致未定义的结果。以下是当存在未决异常时可安全调用的JNI函数的完整列表:

ExceptionOccurred 
ExceptionDescribe 
ExceptionClear 
ExceptionCheck

ReleaseStringChars 
ReleaseStringUTFchars 
ReleaseStringCritical 
Release<Type>ArrayElements 
ReleasePrimitiveArrayCritical 
DeleteLocalRef 
DeleteGlobalRef 
DeleteWeakGlobalRef 
MonitorExit

  前四个函数与异常处理直接相关。剩下的是通用的,它们释放通过JNI公开的各种虚拟机资源。发生异常时通常需要释放资源。

11.8.3 异步异常

一个线程可能通过调用Thread.stop在另一个线程中引发异步异常。异步异常不会影响当前线程中本机代码的执行,直到:

  • 本地代码调用可能引发同步异常的JNI函数之一,或者
  • 本地代码使用ExceptionOccurred来显式检查同步和异步异常。

  只有那些可能引发同步异常的JNI函数才会检查异步异常。

  本地方法可能会在必要的地方插入ExceptionOccurred检查(例如在没有其他异常检查的紧密循环中)以确保当前线程在合理的时间内响应异步异常。

  生成异步异常的Java线程API Thread.stop在Java 2 SDK版本1.2中已被弃用。强烈建议程序员不要使用Thread.stop,因为它通常会导致不可靠的程序。这对于JNI代码尤其是个问题。例如,今天编写的许多JNI库不会仔细地遵循本节中描述的检查异步异常的规则。

发表评论

电子邮件地址不会被公开。 必填项已用*标注

This site uses Akismet to reduce spam. Learn how your comment data is processed.

%d 博主赞过: