Java本地接口(JNI)编程指南和规范(第十一章)

本文深入探讨JNI设计目标、载入本地库、链接本地方法、调用协议、JNIEnv接口指针、数据传递、错误与异常处理等核心内容,旨在为开发者提供全面理解JNI原理与实践的指南。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

原文链接:http://blog.sina.com.cn/s/blog_53988c0c0100ospe.html


第三部分:规范(Part Three: Specification)

 

第十一章"JNI"设计概要(Overview of the JNI Design)
这章给出了"JNI"设计的概要。如果有需要,我们还提供底层技术的动机(technical motivation)。设计概要作为(serve as)主要的"JNI"概念的规范,例如"JNIEnv"接口指针,局部和全局应用,和成员域和方法"IDs"。技术的动机旨在(aim at)帮助读者来理解各种设计的取舍(trade-offs).在有些时候(On a few occasions),我们将讨论怎样实现某个特征。这样的讨论的目的不是表现一个实际实现的策略,而是替代来(instead to)澄清(charify)微妙的语义(subtle semantic)问题。

 

一个桥接不同语言的编程接口的概念不是新的。例如,"C"程序典型地能调用使用例如"FORTRAN"和汇编语言编写的函数。相似地,编程语言例如"LISP"和"Smalltalk"的实现支持各种其他语言的接口。

 

"JNI"解决了一个问题,类似于(similar to)通过被其他语言支持的互操作机制来解决的问题。然而,在"JNI"和在许多其他语言中的互操作机制之间有一明显的不同。"JNI"不是为一个特别的"Java"虚拟器的实现设计的。而是(Rather),一个本地接口能过被每个"Java"虚拟器的实现支持的。我们将进一步详细描述这个,作为我们描述"JNI"设计的目标。

 

11.1 设计目标(Design Goals)
"JNI"设计的最重要目标是确保它在一个给定的主机环境上不同的"Java"虚拟器实现中提供二进制兼容性。同样的本地库的二进制不需要在编译(without the need for recompileation)将运行在一个给定的主机环境上的不同的虚拟器实现上。

 

为了实现这个目标,"JNI"设计不能做任何关于"Java"虚拟器实现的内部细节的假设。因为"Java"虚拟器实现技术是快速发展的(evolve),我们必须小心地避免引进任何限制(introducing any constraints), 限制可能干扰(interfere with)在未来先进的实现技术。

 

"JNI"设计的第二个目的是效率(efficiency)。为了支持时间紧要的代码,"JNI"要实加(impose)尽可能少开销(as little overhead as possible)。然而,我们将清楚我们首先目标,需要实现独立,有时需要我们采取(adopt)一个比我们否则可能的稍微地(slightly)低效的设计。我们在效率和实现的独立之间实现妥协(strike a compromise)。

 

最后,"JNI"必须功能完整(functionally complete)。必须导出足够"Java"虚拟器功能(functionality)来使本地方法和应用程序能完成有用的任务。

 

被一个给定的"Java"虚拟器实现支持的唯一的本地编程接口不是"JNI"的目标。标准接口有益于编程者载入他们的本地代码库到不同的"Java"虚拟器实现。然而,在一些例子中,一个较低层(lower-level)实现特定(implementation-specific)接口可以实现较高的性能。在另一些例子中,编程者可以使用一个较高层的(higher-level)接口来构建软件的控件。

 

11.2 载入本地库(Loading Native Libraries)
当一个应用能调用一个本地方法前,虚拟器必须定位和载入一个本地库,它包含本地方法的实现。

 

12.1.1 类的载入(Class Loaders)
本地库被类载入器(by class loaders)定位(be located)。类载入器在"Java"虚拟器中有许多使用,例如,包括载入类文件,定义类和接口,在软件控件中提供名字空间的分隔,解决在不同类和接口中的符号引用,和最终的定位本地库。我们假设你对类载入器有基本地理解,我们将不去详细描述怎样在"Java"虚拟器中载入和链接它们的类。你能够发现关于类载入器的更多细节在"Dynamic Class Loading in the Java Virtual Machine"论文中,是"Sheng Liang and Gilad Bracha"写的,发表(published)在1998年的关于面向对象的编程系统,语言和应用程序(Object Oriented Programming Systems, Languages, and Applications(OOPSLA)的ACM会议(Conference)的会议录中。

 

类载入器提供名字空间的隔离,被需要在一样虚拟器的一个实例中运行多个控件(例如载入来自不同Web站点的"applets")。通过映射类或接口名字到在"Java"虚拟器中被表示为对象的实际类或接口类型的过程,一个类载入器保持一个隔离名字空间。每个类或接口类型和它定义的载入器相关,载入器初始时读类文件和定义类或接口对象。只有当他们有一样的名字和一样的定义载入器时,两个类或接口类型是一样的。例如,在图11.1中,类载入器"L1"和"L2"每个定义了一个名字为"C"的类。这两个名为"C"的类是不一样的。事实,他们包含两个不同的"f"方法,方法有不同(distinct)返回类型。


            Bootstrap class loader
                       |                                           |
                 |---->| java.lang.String and other system classes |<----|
                 .     |                                           |     .
                 | Class loader L1                                       | Class Loader L2
    | class C{ String f() ;} |                                    | class C { int f() ;} |
Figure 11.1 Two classes of the same name loaded by different class loaders

 

在上图中点线代表在类载入器之间的委托惯性(delegation)。一个类载入器可能请求另一个类载入器来载入它代表的一个类或接口。例如,"L1"和"L2"代表(delegate)为系统类"java.lang.String"的引导类载入器(the bootstrap system classe).代表允许系统类被在所有载入器中共享。这是必需的,因为类的安全性会被破坏(be violated),例如,如果应用程序和系统代码对类型"java.lang.String"是什么有不同的概念(notions)。

 

11.2.2 类载入器和本地库(Class Loaders and Native Libraries)
现在假设在两个"C"类中的方法"f"是本地方法。虚拟器使用"C_f"名字来定位两个C.f方法的本地实现。为确保每个C类链接到正确的本地函数上,每个类载入器必须它自己的一套本地库,像在图11.2中的显示(as shown in Figure 11.2)。


             Bootstrap class loader
                      |                                           |<------------| Native librarays
                |---->| java.lang.String and other system classes |<----|       | associated with
                .     |                                           |     .       | system classes
                | Class loader L1                                       .    ||    ||
    | class C{ String f() ;} |<----|                                    .
                                   |                                    | Class Loader L2
               Native libraries    |                        | class C { int f() ;} |<--|
               associated with L1 | C_f ....|                                          |
                                                                  Native libraries     |
                                                                  associated with L2 | C_f ....|
Figure 11.2 Associating native libraries with class loaders


因为每个类载入器维持一套本地库,所以编程者可以使用功能一个单一库来保存所有的被任何类请求的本地方法,只要这些类有一样定义的载入器。

 

当它们对应的类载入器被垃圾收集时,本地库将自动地被虚拟器载出。

 

11.2.3 定位本地库(Locating Native Libraries)
通过"System.loadLibrary"方法来载入本地库。在下面例子中,"Cls"类的静态初始化载入一个特定平台的本地库,在其中本地方法"f"被定义:
package pkg ;
class Cls{
 native double f(int i, String s) ;
 static{
  System.loadLibrary("mypkg") ;
 }
}

 

"System.loadLibrary"的参数是一个程序员选择的库名字。软件开发者负责选择本地库名字,最小化(minimize)名字冲突的机会(the chance of name clashes)。虚拟器按照一个标准的,而不是特定的主机环境的,协议(convention)来转换库名字到一个本地库名字。例如,"Solaris"操作系统转换"mypkg"名字到"libmypkg.so",然而"Win32"操作系统转换一样的名字"mypkg"到"mypkg.dll"。

 

当"Java"虚拟器启动,它构建一个目录列表,被使用来为应用程序类定位的本地库。这列表的内容依赖于主机环境和虚拟器实现。例如,在"Win32 JDK"或"Java 2 SDK releases"下,目录列表包含"Windows"的系统目录,当前工作目录,和在"PATH"环境变量中的实体。在"Solaris JDK or Java 2 SDK releases"下,目录列表包含在"LD_LIBRARY_PATH"环境变量中的实体。

 

如果它载入命名的本地库失败(fail to load the named native library),"System.loadLibrary"抛出一个"UnsatisfiedLinkError"。如果一个较早的"System.loadLibrary"调用已经载入一样本地库,"System.loadLibrary"安静地完成。如果底层操作系统不支持动态链接,所有本地方法必须被预链接到虚拟器上。在这个例子中,虚拟器完成"System.loadLibrary"调用,但不实际地载入库。

 

为每个类载入器,虚拟器内部地保持了一个载入的本地库的列表。下面三步决定哪个类载入器应该被连接到一个最新载入本地库上:
1.确定"System.loadLibrary"的直接调用者(immediate caller)
2.识别定义调用者的类
3.得到调用者类定义的载入器。

 

在下面例子中,本地库"foo"将和"C"定义的载入器相关:
class C {
 static{
  System.loadLibrary("foo");
 }
}

 

"Java 2 SDK release 1.2"导入了一个新的"ClassLoader.findLibrary"方法,来允许编程者指定一个自定义的库载入规则,规则详细说明了一个给定的类载入器。"ClassLoader.findLibrary"方法有一个独立于平台的库名(例如(such as)"mypkg")为参数,同时:
.或者返回"null"来命令虚拟器,使用默认的库搜索路劲。
.或者返回一个依赖于主机环境的库文件的绝对路径(例如"c:\\mylibs\\mypkg.dll")。

 

"ClassLoader.findLibrary"通常和在"Java 2 SDK release 1.2"中添加的另一种方法"System.mapLibraryName"被一起使用的。"System.mapLibraryName"映射独立于平台的库名(例如"mypkg")到依赖平台的库文件名字(例如"mypkg.dll")。

 

在"Java 2 SDK release 1.2",通过设置"java.library.path"特性(property),你能重载默认的库搜索路径。例如,下面的命令行启动(start up)一个程序"Foo",它需要在"c:\mylibs"目录中载入一个本地库:
java -Djava.library.path=c:\mylibs Foo

 

11.2.4 一个类型安全性限制(A Type Safety Restriction)
虚拟器不允许一个给定的本地库被不止一个类载入器载入。通过多个类载入器,尝试载入一样的本地库导致一个"UnsatisfiedLinkError"异常被抛出。这个限制的目的是确保,基于类载入器的名字空间隔离被保存在本地库中。没有这个限制,通过本地方法,比较容易错误地混用来自不同的类载入器的类和接口。考虑一个本地方法"Foo.f"在一个全局引用中缓冲它自己定义的类:
JNIEXPORT void JNICALL
Java_Foo_f(JNIEnv *env, jobject self)
{
 static jclass cachedFooClass ;
 
 if ( cachedFooClass == NULL ){
  jclass fooClass = (*env)->FindClass(env, "Foo") ;
  if( fooClass == NULL ){
   return ;
  }
  cachedFooClass = (*env)->NewGlobalRef(env, fooClass) ;
  if( cachedFooClass == NULL){
   return ;
  }
  assert( (*env)->IsInstatnceOf(env, self, cachedFooClass)) ;
  ...
 }
}

 

我们期望断言是成功的,因为"Foo.f"是一个实例方法和"self"是"Foo"的实例。然而,这个断言能够失败,如果两个不同的"Foo"类被类载入器"L1"和"L2"载入,同时两个"Foo"类都链接到前面的"Foo.f"实现。为第一次调用"f"方法的"Foo"类,"CachedFooClass"全局引用将被创建。另一个"Foo"类的"f"方法的后来调用将引起断言失败。

 

在类载入器中,"JDK release 1.1"没有正确地强制本地方法隔离。这意味着对于在不同类载入器中两个类可能链接到一样本地方法。当前面例子显示时,在"JDK release 1.1"中这个方法导致下面两个问题:
.一个类可能错误地(miskakenly)链接到本地库,在一个不同类载入器中,它被一个类用一样名字载入。
.本地方法可能容易混淆来自不同类载入器的类。这破坏了类载入器提供的名字空间的间隔,同时倒是类安全问题。

 

11.2.3 载出本地库(Unloading Native Libraries)
在虚拟器垃圾收集(garbage collect)和本地库相关的类载入器,虚拟器载出一个本地库。因为类使用它们定义的载入器,这暗示虚拟器也已经载出了类,这个类有静态初始化调用"System.loadLibrary"和载入本地库(11.2.2部分)。

 

11.3 链接本地方法(Linking Native Methods)
在第一次调用每个本地方法前,虚拟器尝试链接每个本地方法。一个本地方法"f"能被链接的最早的时间是一个方法"g"的第一次调用,那儿是来自"g"到"f"的方法体的一个引用。虚拟器实现不应该太早地链接一个本地方法。因为实现本地方法的本地库不能被载入,如此做可能导致不能预期的链接错误。

 

链接一个本地方法包括(involve)下面步骤:
.确定定义本地方法的类的类载入器。
.搜索和这个类载入器相关的本地库集来定位实现本地方法的本地函数。
.建立内部数据结构,使本地方法的所有将来调用将直接地跳到本地函数。

 

通过连接(concatenate)下面的控件,虚拟器从本地方法的名字中推得(deduce)本地函数的名字:
.前缀"Java_"
.一个编码的完全(fully)合格(qualified)类名字
.一个下划线隔开
.一个编码的方法名字
.为重载本地方法,双下划线跟随编码参数描述符

 

通过和定义的载入器相关的所有本地库,虚拟器搜索一个恰当名字的本地函数。为每个本地库,虚拟器首先须寻找短名字,是没有参数描述符的名字。然后,寻找带有参数描述符的长名字。只当一个本地函数被用另一个本地方法重载时,程序员需要使用长名字。然而,如果本地方法是对非本地方法的重载,这不是一个问题。后者(非本地方法)不是驻留在本地库中。

 

在下面的例子中,本地方法"g"不必使用长名字来链接,因为另一个方法"g"不是一个本地方法。
class Clsl{
 int g(int i){ ... } // regular method
 native int g(double d) ;
}

 

"JNI"采用(adopt)一个简单的名字编码方法来确保所有"Unicode"字符转化为(translate into)有效的"C"函数名。下划线(underscore)字符隔开完全合格的类名字的控件。因为一个名字或类型描述符没有一个数字的开始,我们能使用_0,...,_9来转义序列(escape sequences),如下说明(as illustrated below):
Escape Sequence(转义序列)                    Denotes(指示)
_0xxxx                                      a Unicode charater XXXX(一个"Unicode"字符 "xxxx"值)
_1                                          the charater "_"(下划线字符)
_2                                          the charater ";" in descriptors(在描述符中的分号字符)
_3                                          the charater "[" in descriptors(在描述符中的[字符)

 

如果匹配一个编码本地方法名字的本地函数出现在多个本地库中,在第一个载入的本地库中的函数被链接到本地方法。如果没有函数匹配本地方法名字,一个"UnsatisfiedLinkError"被抛出。

 

编程者也能调用"JNI"函数"RegisterNatives"来注册本地方法链接到一个类。"RegisterNatives"函数对于静态链接函数是特别有用的。

 

11.4 调用协议(Calling Conventions)
调用协议决定一个本地函数怎样接受参数和返回结果。在不同本地语言中,或相同语言的不同实现中,没有标准调用协议。例如对于不同的C++编译器一般产生允许不同调用协议的代码。

 

如果可能,需要"Java"虚拟器和广泛不同的本地调用协议互操作是困难的。在一个给定主机环境上使用一个指定的标准调用协定来写"JNI"请求本地方法。例如,"JNI"在"UNIX"上按照"C"调用协定,同时在"Win32"上按照"stdcall"协定。

 

当程序员需要调用使用(follow)不同调用协定的函数时,他们必须写存根(stub)函数,来使适应(adapt)恰当的本地语言的函数的"JNI"调用协定。

 

11.5 "JNIEnv"接口指针(The JNIEnv Interface Pointer)
通过"JNIEnv"接口到处的不同函数的调用,本地代码来访问虚拟器功能(functionality)。

 

11.5.1 "JNIEnv"接口指针的组织(Organization of the JNIEnv Interface Pointer)
一个"JNIEnv"接口指针是一个线程局部数据的指针,它又包含一个函数表的值指针。每个接口函数是在表中一个预定义偏移上。"JNIEnv"接口的组织像一个"C++"虚拟函数表,同时也像一个"Microsoft COM"接口。图11.3,说明"JNIEnv"接口指针的集。


Thread #1's JNIEnv interface pointer
       |-->| pointer        |---------------------------------|
           | per-thread data|                                 |
Thread #2's JNIEnv interface pointer                          |
       |-->| pointer        |---------------------------------|
           | per-thread data|                                 |
             ...                                                     .
Thread #n's JNIEnv interface pointer                          |
       |-->| pointer        |---------------------------------|   Table of JNI functions                
           | per-thread data|                                 | 
                                                              |->| pointer |-> interface function
                                                                 | pointer |-> interface function
                                                                 | pointer |-> interface function
                                                                 | ...     |
Figure 11.3 Thread Loacl JNIEnv Interface Pointers

 

实现一个本地方法的函数接受"JNIEnv"接口指针作为它们的第一个参数。保证虚拟器传递一样的接口指针给从一样线程调用的本地方法实现函数。然而,一个本地方法可能被来自不同线程的调用,因此可能被传地不同"JNIEnv"接口指针。虽然接口指针是线程局部的(thread-local),双重间接的"JNI"函数表被共享在多线程中。

 

"JNIEnv"接口指针看做一个线程局部结构的原因是一些平台没有对线程局部存储访问的有效支持。通过绕过(pass around)一个线程局部指针,在虚拟器中的"JNI"实现能够避免许多线程局部存储访问操作,否则必须执行这些操作。

 

因为"JNIEnv"接口指针是线程局部的,本地代码不该在另一个线程中使用属于这个线程的"JNIEnv"接口指针。本地代码可以使用"JNIEnv"指针作为一个线程ID, 线程ID对于线程的生命期是保持唯一的。

 

11.5.2 一个接口指针的好处(Benefits of an Interface Pointer)
这儿是使用一个接口指针的一些好处,是相对于(as opposed to)硬函数实体的(hardwired function entries):
.最重要地,因为"JNI"函数表是作为一个参数传递给每个本地方法,本地库不必和一个"Java"虚拟器的特殊实现链接。这是关键,因为不同卖主可以命名不同的虚拟器的实现。每个库自包含,是为一样的本地二进制库(the same native library binary)和在一个给定主机环境上来自不同卖主的虚拟器实现一起运行的先决条件(a prerequisite)。


.其次,通过不适用硬函数实体,虚拟器实现可以选择提供多个版本的"JNI"函数表。例如,虚拟器实现可以支持两个"JNI"函数表:一个执行彻底的(thorough)不合逻辑的参数检测,同时适合调试;另一个执行最少的被"JNI"规则请求的检测,同时因此更有效率。"Java 2 SDK release 1.2"支持一个"-Xcheck":"jni"选项(option)选择地(optionally)为"JNI"函数打开(turn on)额外的检测。


.最后,多个"JNI"函数表可以支持将来的类似"JNIEnv"接口的多个版本。虽然我们不能预见(foresee)需要做的,"Java"平台的将来版本能支持一个新的"JNI"函数表,除了被在"1.1 and 1.2 releases"中的"JNIEnv"接口指向的函数表。"Java 2 SDK release 1.2"导入一个"JNI_Onload"函数,它是被一个本地函数定义的来指示(indicate)被本地函数需要的"JNI"函数表的版本。"Java"虚拟器的将来能够同时支持多个版本的"JNI"函数表,同时传递正确版本到依赖他们需要的单独的(individual)本地库。

 

11.6 传递数据(Passing Data)
基本数据类型,例如整数(intergers),字符(charaters),等等(and so on),被在"Java"虚拟器和本地代码之间复制。在一方面上的对象通过引用传递。每个引用包含一个直接的指向底层对象的指针。对象的指针(The pointer to object)不能被本地代码直接使用。来自本地代码视角,引用是透明的(references are opaque)。

传递引用,替代直接的对象直接指针,使虚拟器能用更灵活的(flexible)方法来管理对象。图11.4,说明(illustrate)一个引用如此灵活。当本地代码持有一个引用时,虚拟器可以执行一个垃圾收集,导致一个对象被从记忆体的一块地方复制到另一块地发。虚拟器能自动地更新引用的内容,使即使对象已经移动了,引用任然是有效的。

 

reference                                     reference
  |                                               |                    ...........
  |--->|    -|---->|   object   |                 |--->|     -|----|   . (moved) .
                                                                   |   ...........
After the garbage collector         ----|                          |
moves the object, the virual            |->                        |-->|   object   |
machine automatically updates       ----|
the reference.
Figure 11.4 Relocating an Object while Native Code Holds a Reference(重新部署一个对象,当本地代码有一个引用时)

 

11.6.1 全局和局部引用(Global and Local Reference)
"JNI"为本地代码创建两种类型的对象引用:局部和全局引用。局部引用对于一个本地方法调用的期间(duration)是有效的,同时在本地方法返回后自动释放。全局引用保持有效,直到他们被明显地释放。

 

对象作为局部引用传递给本地方法。大多"JNI"函数返回局部引用。"JNI"允许编程者从局部引用来创建全局引用。把对象作为参数的"JNI"函数接受全局和局部引用。一个本地方法可以返回一个局部或一个全局引用到虚拟器作为它的结果。

 

局部引用只创建它们的线程中有效。本地代码不该传递局部引用从一个线程到另一线程(Frome one thread to another)。

 

在"JNI"中一个"NULL"引用相当于在"Java"虚拟器中的"null object"。一个值为非空的局部或全局引用不是一个"null object"。

 

11.6.2 实现局部引用(Implementing Local References)
为实现局部引用,为每个从虚拟器到一个本地方法的过渡控制,"Java"虚拟器创建一个登记(registry)。一个登记映射不能移动局部引用的对象指针。在登记中对象不能被垃圾收集。被传递给本地方法的所有对象,包括作为"JNI"函数调用的结果被返回的对象,被自动地加入登记。在方法返回后登记被删除,允许它的实体被垃圾收集。图11.5说明,局部引用登记怎样被创建和被删除。"Java"虚拟器框架对应的本地方法包含一个指向局部引用的登记。一个方法"D.f"调用本地方法"C.g"。"C.g"是被"C"函数"Java_C_g"实现的。在进入"Java_C_g"前,虚拟器创建一个局部引用登记,同时在"Java_C_g"返回后,删除局部引用登记。

 

Java       | ....  |        | ...  |                            | ...  |
virtual    | D.f   |        | D.f  |                            | D.f  |
machine                     | C.g -|-----|
frames                                   |-->|     |
                     ---->                   | ... |    ---> 
                                             local
                                             reference
Native                      | Java_C_g |     registry
frames                      | ...      |
       Before calling                                        Returned from
       native method        in the native  method            native method
Figure 11.5 Creating and Deleting a Local Reference Registry

 

有不同的方法来实现一个登记,例如使用一个栈,一个表,一个链接列,或一个哈希(hash)表。虽然引用计数可以被用来避免在登记中有相同的(duplicated)实体(entries),一个"JNI"的实现无责任来(be obliged to)侦测和去除(collapse)重复实体(duplicate entries)。

 

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

 

11.6.3 弱全局引用(Weak Global Reference)
"Java 2 SDK release 1.2"导入一个新的全局引用类:弱全局引用。不像一般全局引用,一个弱全局引用允许一个引用对象被垃圾收集。在底层对象的垃圾收集后,一个弱引用被清除。本地代码能够通过使用"IsSameObject"来比较引用和NULL来检测一个弱引用是不是被清除。

 

11.7 访问对象(Accessing Objects)
"JNI"提供为引用到对象提供了一套丰富的访问函数。这意味着无论虚拟器内部怎样表示对象,都有一样本地函数方法实现。这是至关重要的(crucial)设计决定(decision),能使"JNI"被任何虚拟器实现来支持。

 

通过透明的(opaque)引用使用访问函数的开销(overhead)高于直接访问"C"数据结构的开销。我们相信,大多数情况(in most cases),本地方法执行平凡的任务,掩盖了外部函数调用的费用。

 

11.7.1 访问基本类型数组(Accessing Primitive Arrays)
然而,对于重复访问在巨大对象中的基本类型数据类型的值,例如整型数组和字符串,函数调用开销是不能接受的。考虑一下被用来执行向量和矩阵(vector and matrix)计算的本地方法。通过一个整数数组的迭代(iterate)同时用一个函数调来返回每一个元素是非常(grossly)效率低下的(inefficient)。

 

一个解决方法导入一个寄托("pinning")的概念,使本地方法能够请求虚拟器不移动一个数组的内容。然后,本地方法得到一个指向元素组的指针。然而,这个方法(approach)有两个影响(implication):
.垃圾收集必修支持寄托(pinning)。在许多实现中,寄托(pinning)是不受欢迎的(undesirable),因为它使垃圾收集算法(algorithms)变复杂(complicate),同时导致内存碎片(fragmentation)。
.虚拟器必须连续地(contiguously)安排(lay out)基本类型数组在内存中。虽然(Although)这是对于大多数基本类型数组的正常(natural)实现,但"boolean"数组能像封包或解包一样被实现。一个封包的"boolean"数组使用一个"bit"为每个元素,然后(whereas)一个解包"boolean"数组典型地使用一个"byte"为每一个元素。因此,依赖(rely on)"boolean array"的特别安排的本地代码将不能被移植(portable)。

 

"JNI"采用一个妥协(compromise)方法来解决(address)上面两个问题。

 

首先,"JNI"提供一套函数(例如,GetIntArrayRegion and SetIntArrayRegion)来在一段基本类型数组和一个本地内存缓存之间复制基本类型数组元素。如果本地方法需要访问在一个大的数组中的一部分元素,或者如果本地方法需要复制一份这个数组,使用这些函数。

 

其次,编程者能使用另一套函数(例如,"GetIntArrayElements")来尝试得到一个"pinned"版本的数组元素。然而,依赖于虚拟器的实现,这些函数可以引起存储的分配和复制。事实上这些函数是否复制数组,依赖于虚拟器实现如下(as follows):
.如果垃圾收集支持"pinning",数组的安排是和一样类型的本地数组的安排一样,然而不需要复制。
.否则,数组被复制到一个不能移动的内存块(例如,在"C"堆上)同时执行必须的格式化转化。返回一个副本的指针。

 

本地代码调用第三套函数(例如,"ReleaseIntArrayElements")来通知虚拟器,本地代码不再需要访问数组元素。当这个发生时,虚拟器"unpin"数组或者调和带有不可移动的副本的原始数组,同时释放副本。

 

这个方法提供了灵活性。一个垃圾收集算法能独立决定为数组的复制(copying)或"pinning"。在一个特别实现的设计(sheme)下,垃圾收集可能复制小的数组,但"pin"大的数组。

 

最后,"Java 2 SDK release 1.2"导入两个新函数:"GetPrimitiveArrayCritical and ReleasePrimitiveArrayCritical"。这些函数能以类似的方法,例如,"GetIntArrayElements and ReleaseIntArrayElements",来被使用。然而,在它使用"GetPrimitiveArrayCritical"来得到数组元素的一个指针后,和在它使用"ReleasePrimitiveArrayCritical"来释放指针前,在本地代码上有重要的限制。在一个限制域内(Inside a "critical region"),本地代码不应该运行一个不定期的时间片段(即定时函数),不该调用任意的"JNI"函数,同时不该在虚拟器上执行可以引起当前线程阻塞和等待另一个线程的操作。给出这些限制,虚拟器能够临时地使垃圾收集无效,当让本地代码直接访问数组元素时。因为不需要"pinning"支持,"GetPrimitiveArrayCritical"更适合(be more likely to)返回一个直接的基本类型数组元素的指针,比,例如,"GetIntArrayElements"。

 

"JNI"实现必须确保,运行在多线程中的本地方法能同时地(simultaneously)访问一样数组。例如,"JNI"可以为每个"pinned(被固定)"数组保持一个内部计数器,使一个线程不会"unpin"一个被另一线程还"pinned"的数组。注意"JNI"不需要锁住基本类型数组为一个本地方法独占的访问。同时更新来自另一个线程的数组是允许的,虽然这导致不确定的结果(nondeterministic result)。

 

11.7.2 成员域和方法(Fields and Methods)
"JNI"允许本地代码访问在"Java"编程语言中定义的成员域和方法。"JNI"通过他们的符号名字和类型描述符来标记方法和成员域。一个两步处理分解出从它的名字和描述符定位成员域或方法的成本。例如,读一个整数实例成员域"i"在类"cls"中,首先本地代码得到一个成员域ID,如下:
jfieldID fid = env->GetFieldID(env, cls, "i", "I") ;

 

然后本地代码能重复地(repeatedly)使用成员域ID,没有查询成员域的成本,如下:
jint value = env->GetIntField(env, obj, fid) ;

 

一个成员域或方法ID保持有效,直到虚拟器载出定义对应的成员域或方法的这个类或接口。这个类或接口被载出后,方法或成员域ID变的无效。

 

编程者能从拥有对应成员域或方法的类或接口得到一个成语域或方法ID。成员域或方法能被定义在它自己的类或接口中或继承来自父类或父接口。"The Java Virtual Machine Specification"包含了解决成员域和方法的正确的(precise)规则。如果两个类或接口定义了一样的成员域或方法,"JNI"实现一定从这两个类或接口为一个给定名字和描述符,得到一样的成员域ID或方法ID。例如,如果"B"定义成员域"fld",同时"C"从"B"继承了"fld",然后编程者保证从类"B"和"C"中为成员域名"fld"得到一样成员域ID。

 

"JNI"不会在成员域和方法IDs内部怎样实现上做任何限制。

 

注意你需要成员域名字和成员域描述符来从一个给定类或接口中得到一个成员域ID。这可能看似不必,因为成员域不能在"Java"编程语言中被重载。然而,在一个类文件中重载成员域,和在"Java"虚拟器上运行如此类文件,是合法的。因此,"JNI"能够处理合法类文件,但这不是为"Java"编程语言的一个编译器产生的。

 

如果编程者已经知道方法或成员域的名字和类型,编程者只能使用"JNI"来调用方法或访问成员域。相比之下(In comparison),"the Java Core Reflection API"允许编程者来决定在一个被给的类或接口中的成员域和方法的设置。同时在本地代码中有时能够反映类或接口的类型。"Java 2 SDK release 1.2"提供新的"JNI"函数,他们被设计成和存在的"Java Core Reflect API"一起工作。新的函数包含一对在"JNI"成员域IDs和"java.lang.reflect.Field"类的实例之间转换函数,和另一对在"JNI"方法IDs和"java.lang.reflect.Method"类的实力之间转换的函数。

 

11.8 错误和异常(Errors and Exceptions)
在"JNI"程序中出现的错误不同于在"Java"虚拟器实现中发生的异常。编程者错误是"JNI"函数的错误使用(misuses of JNI functions)引起的。例如,编程者可能错误地传递一个对象引用,替代了一个类引用,给"GetFieldID"。例如,通过通过"JNI"当本地代码尝试分配一个对象时,发生内存不够的情况时,虚拟器异常产生。

 

11.8.1 对于编程的错误不检查(No Checking for Programming Errors)
"JNI"函数不检查编程的错误。传递不合法的参数给"JNI"函数导致(result in)未定义的行为。这样设计决定的原因(The reason for this design decision)如下(as follows):
.强制"JNI"函数来检查所有可能错误条件,在所有(特别正确)本地方法上降低了性能(degrade the performance)。
.在许多情况,没有足够运行类型信息来执行如此检查。

 

大多"C"库函数没有防止(guard against)编程错误。例如,"printf"函数,当它收到一个无效的地址,通常触发(trigger)一个运行错误,替代了返回一个错误代码。强制"C"库函数来检查(check for)所有可能错误情况,可能会(would likely)导致如此检查重复,先(once)在用户代码中和然后(then)再在库中。

 

虽然"JNI"规范(specification)不要求虚拟器来检查编程的错误(errors),虚拟器实现被鼓励来提供检查一般的错误(mistakes)。例如,一个虚拟器可以在"JNI"函数表的调试版本的中执行更多的检查(11.5.2部分)。

 

11.8.2 "Java"虚拟器异常(Java Virtual Machine Exceptions)
"JNI"在本地编程语言中不依赖域异常处理机制(exception handling mechanisms)。通过调用"Throw"或"ThrowNew",本地代码可以引起"Java"虚拟器抛出一个异常。在当前线程中记录一个悬而未决的异常。不像在"Java"编程语言中抛出的异常,在本地代码中抛出的异常不会立即使当前执行中断(disrupt)。

 

在本地语言中没有标准的异常处理机制。因此(Thus),在每个能潜在抛出一个异常的操作后,"JNI"编程者被期望检查和处理异常。"JNI"编程者可以用两种方法处理异常:
.在初始化本地方法调用的代码中,本地方法可以选择立即返回,产生异常被抛出。
.本地代码可以通过调用"ExceptionClear"来清除异常,然后执行它自己的异常处理代码。

 

在调用任何后续(subsequence)"JNI"函数前,最重要是检查,处理和清除一个悬而未决的异常。在带有一个悬而未决异常调用大多"JNI"函数,导致未定义的结果。下面是,在有一个悬而未决(pending)异常时,能过安全地调用的"JNI"函数的完成列表:
ExceptionOccurred
ExceptionDescribe
ExceptionClear
ExceptionCheck

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

 

开始的四个函数是直接地和异常处理相关的。剩下的函数通常是通过"JNI"导出的,在其中他们释放各种虚拟器资源。在异常发生时,经常必须能过释放资源。

 

11.8.3 异步异常(Asynchronous Exceptions)
在另一个线程中通过调用"Thread.stop",一个线程可能产生一个异步异常。一个异步异常不会影响在当前线程中的本地代码的执行,直到:
.本地代码调用一个"JNI"函数,它能产生同步异常,或
.本地代码使用"ExceptionOccurred"来明确地(explicitly)检查同步或异步异常。

 

只有这些能产生潜在地同步异常的"JNI"函数,检查异步异常。

 

本地方法可以在必要的地方(in necessary places)(例如在没有异常检查的紧密循环中),插入"ExceptionOccurred"检查,来确保当前线程相应(respond to)异步异常在一个合理的时间段内。

 

产生(generate)异步异常的"Java thread API","Thread.stop",在"Java 2 SDK release 1.2"中已经过时了(be deprecated)。编程者被强烈阻止(be discouraged from)使用"Thread.stop",因为它一般导致不可信任的(unreliable)编程。这对于"JNI"代码是一个特别的问题。例如,许多今天写的"JNI"库不细心按照规则来检查在这章中描述的异步异常。


评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值