java JNI编程指南 下载本文

1、 2、 3、

确定System.loadLibrary的调用者。 确定定义调用者的类。 确定类的加载器。

下面的例子中,JVM会把本地库foo和定义C的类加载器关联起来。

class C {

static {

System.loadLibrary(\ } }

11.2.4 类型安全保障措施

VM中规定,一个JNI本地库只能被一个类加载器加载。当一个JNI本地库已经被第一个类加载器加载后,第二个类加载器再加载时,会报UnsatisfiedLinkError。这样规定的目的是为了确保基于类加载器的命令空间分隔机制在本地库中同样有效。如果不这样的话,通过本地方法进行操作JVM时,很容易造成属于不同类加载器的类和接口的混乱。下面代码中,本地方法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, \ 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的实例对象,所以,我们认为最后那个assertion会执行成功。但是,如果L1和L2分别加载了两个不同的Foo类,而这两个Foo类都被链接到Foo.f的实现上的话,assertion可能会执行失败。因为,哪个Foo类的f方法首先被调用,全局引用cachedFooClass指向的就是哪个Foo类。

11.2.5 unload本地库

一旦JVM回收类加载器,与这个类加载器关联的本地库就会被unload。因为类指向它自己的加载器,所以,这意味着,VM也会被这个类unload。

11.3 链接本地方法

VM会在第一次使用一个本地方法的时候链接它。假设调用了方法g,而在g的方法体中出现了对方法f的调用,那么本地方法f就会被链接。VM不应该过早地链接本地方法,因为这时候实现这些本地方法的本地库可能还没有被load,从而导致链接错误。 链接一个本地方法需要下面这几个步骤: 1、 2、 3、 1、 2、 3、 4、 5、

确定定义了本地方法的类的加载器。

在加载器所关联的本地库列表中搜索实现了本地方法的本地函数。 建立内部的数据结构,使对本地方法的调用可能直接定向到本地函数。 前缀“Java_”。 类的全名。

下划线分隔符“_”。 方法名字。

有方法重载的情况时,还会有两个下划线(“__”),后面跟着参数描述符。

VM通过下面这几步,同本地方法的名字生成与之对应的本地函数的名字:

VM在类加载器关联的本地库中搜索符合指定名字的本地函数。对每一个库进行搜索时,VM会先搜索短名字(short name),即没有参数描述符的名字。然后搜索长名字(long name),即有参数描述符的名字。当两个本地方法重载时,程序员需要使用长名字来搜索。但如果一个本地方法和一个非本地方法重载时,就不会使用长名字。

JNI使用一种简单的名字编码协议来确保所有的Unicode字符都被转化成可用的C函数名字。用下划线(“_”)分隔类的全名中的各部分,取代原来的点(“.”)。

如果多个本地库中都存在与一个编码后的本地方法名字相匹配的本地函数,哪个本地库首先被加载,则它里面的本地函数就与这个本地方法链接。如果没有哪个函数与给定的本地方法相匹配,则UnsatisfiedLinkError被抛出。

程序员还可以调用JNI函数RegisterNatives来注册与一个类关联的本地方法。这个JNI函数对静态链接函数非常有用。

11.4 调用转换(calling convention)

调用转换决定了一个本地函数如何接收参数和返回结果。目前没有一个标准,主要取决于编译器和本地语言的不同。JNI要求同一个系统环境下,调用转换机制必须相同。例如,JNI在UNIX下使用C调用转换,而在Win32下使用stdcall调用转换。

如果程序员需要调用的函数遵循不同的调用转换机制,那么最好写一个转换层来解决这个问题。

11.5 JNIEnv接口指针

JNIEnv是一个指向线程局部数据的接口指针,这个指针里面包含了一个指向函数表的指针。在这个表中,每一个函数位于一个预定义的位置上面。JNIEnv很像一个C++虚函数表或者Microsoft COM接口。图11.3演示了这种关系。

图11.3 线程的局部JNIEnv接口指针

如果一个函数实现了一个本地方法,那么这个函数的第一个参数就是一个JNIEnv接口指针。从同一个线程中调用的本地方法,传入的JNIEnv指针是相同的。本地方法可能被不同的线程调用,这时,传入的JNIEnv指针是不同的。但JNIEnv间接指向的函数表在多个线程间是共享的。 JNI指针指向一个线程内的局部数据结构是因为一些平台上面没有对线程局部存储访问的有效支持。

因为JNIEnv指针是线程局部的,本地代码决不能跨线程使用JNIEnv。

11.5.2 接口指针的好处

比起写死一个函数入口来说,使用接口指针可以有以下几个优点: 1、 2、

JNI函数表是作为参数传递给每一个本地方法的,这样的话,本地库就不必与特定的JVM可以提供几个不同的函数表,用于不同的场合。比如,JVM可以提供两个版本JVM关联起来。这使得JNI可以在不同的JVM间通用。

的JNI函数表,一个做较多的错误检查,用于调试时;另外一个做较少的错误检查,更高效,用于发布时。

11.6 传递数据

像int、char等这样的基本数据类型,在本地代码和JVM之间进行复制传递,而对象是引用传递的。每一个引用都包含一个指向JVM中相应的对象的指针,但本地代码不能直接使用这个指针,必须通过引用来间接使用。

比起传递直接指针来说,传递引用可以让VM更灵活地管理对象。比如,你在本地代码中抓着一个引用的时候,VM那小子可能这个时候正偷偷摸摸地把这个引用间接指向的那个对象从一块儿内存区域给挪到另一块儿。不过,有一点儿你放心,VM是不敢动对象里面的内容的,因为引用的有效性它要负责。瞅一下图11.4,你就会得道了。

图11.4 本地代码抓着引用时,VM的偷鸡摸狗

11.6.1 全局引用和局部引用这对好哥儿们

本地代码中,可以通过JNI创建两种引用,全局引用和局部引用。局部引用的有效期是本地方法的调用期间,调用完成后,局部引用会被JVM自动铲除。而全局引用呢,只要你不手动把它干掉,它会一直站在那里。

JVM中的对象作为参数传递给本地方法时,用的是局部引用。大部分的JNI函数返回局部引用。JNI允许程序员从局部引用创建一个全局引用。接受对象作为参数的JNI函数既支持全局引用也支持局部引用。本地方法执行完毕后,向JVM返回结果时,它可能向JVM返回局部引用,也可能返回全局引用。

局部引用只在创建它的线程内部有效。本地代码不能跨线程传递和使用局部引用。

JNI中的NULL引用指向JVM中的null对象。对一个全局引用或者局部引用来说,只要它的值不是NULL,它就不会指向一个null对象。

11.6.2 局部引用的内部实现

一个对象从JVM传递给本地方法时,就把控制权移交了过去,JVM会为每一个对象的传递创建一条记录,一条记录就是一个本地代码中的引用和JVM中的对象的一个映射。记录中的对象不会被GC回收。所有传递到本地代码中的对象和从JNI函数返回的对象都被自动地添加到映射表中。当本地方法返回时,VM会删除这些映射,允许GC回收记录中的数据。图11.5演示了局部引用记录是怎么样被创建和删除的。一个JVM窗口对应一个本地方法,窗口里面包含了一个指向局部引用映射表的指针。方法D.f调用本地方法C.g。C.g通过C函数Java_C_g来实现。在进入到Java_C_g之前,虚拟机会创建一个局部引用映射表,当Java_C_g返回时,VM会删掉这个局部引用映射表。

图11.5 创建和删除局部引用映射表

有许多方式可以实现一个映射表,比如栈、表、链表、哈希表。实现时可能会使用引用计数来避免重得。

11.6.3 弱引用

弱引用所指向的对象允许JVM回收,当对象被回收以后,弱引用也会被清除。

11.7 对象访问

JNI提供丰富的函数让本地代码通过引用来操作对象,而不用操心JVM内部如何实现。使用JNI函数来通过引用间接操作对象比使用指针直接操作C中的对象要慢。但是,我们认为这很值得。

11.7.1 访问基本类型数组

访问数组时,如果用JNI函数重复调用访问其中的每一个元素,那么消耗是相当大的。 一个解决方案是引入一种“pin”机制,这样JVM就不会再移动数组内容。本地方法接受一个指向这些元素的直接指针。但这有两个影响: 1、 2、

JVM的GC必须支持“pin”。“pin”机制在JVM中并不是一定要实现的,因为它会使JVM必须在内存中连续地存放数组。虽然这是大部分基本类型数组的默认实现方式,GC的算法更复杂,并有可能导致内存碎片。

但是boolean数组是比较特殊的一个。Boolean数组有两种方式,packed和unpacked。用packed实现方式时,每个元素用一个bit来存放一个元素,而unpacked使用一个字节来存放一个元素。因此,依赖于boolean数组特定存放方式的本地代码将是不可移植的。 JNI采用了一个折衷方案来解决上面这两个问题。

首先,JNI提供了一系列函数(例如,GetIntArrayRegion、SetIntArrayRegion)把基本类型数组复制到本地的内存缓存。如果本地代码需要访问数组当中的少量元素,或者必须要复制一份的话,请使用这些函数。

其次,程序可以使用另外一组函数(例如,GetIntArrayElement)来获取数组被pin后的直接指针。如果VM不支持pin,这组函数会返回数组的复本。这组函数是否会复制数组,取决于下面两点: