字段ID在类C的静态初始时被计算并缓存下来,这样就可以确保缓存的是C.i的ID,因此,不管本地方法中接收到的jobject是哪个类的实例,访问的永远是C.i的值。 另外,同样的情况也可能会出现在方法ID上面。
10.8 Unicode字符串结尾
从GetStringChars和GetStringCritical两个方法获得的Unicode字符串不是以NULL结尾的,需要调用GetStringLength来获取字符串的长度。一些操作系统,如Windows NT中,Unicode字符串必须以两个’\\0’结尾,这样的话,就不能直接把GetStringChars得到的字符串传递给Windows NT系统的API,而必须复制一份并在字符串的结尾加入两个“\\0”
10.9 访问权限失效
在本地代码中,访问方法和变量时不受JAVA语言规定的限制。比如,可以修改private和final修饰的字段。并且,JNI中可以访问和修改heap中任意位置的内存。这些都会造成意想不到的结果。比如,本地代码中不应该修改java.lang.String和java.lang.Integer这样的不可变对象的内容。否则,会破坏JAVA规范。
10.10 忽视国际化
JVM中的字符串是Unicode字符序列,而本地字符串采用的是本地化的编码。实际编码的时候,我们经常需要使用像JNU_NewStringNative和JNU_GetStringNativeChars这样的工具函数来把Unicode编码的jstring转化成本地字符串,要对消息和文件名尤其关注,它们经常是需要国际化的,可能包含各种字符。
如果一个本地方法得到了一个文件名,必须把它转化成本地字符串之后才能传递给C库函数使用:
JNIEXPORT jint JNICALL
Java_MyFile_open(JNIEnv *env, jobject self, jstring name, jint mode) {
jint result;
char *cname = JNU_GetStringNativeChars(env, name); if (cname == NULL) { return 0; }
result = open(cname, mode); free(cname); return result; }
上例中,我们使用JNU_GetStringNativeChars把Unicode字符串转化成本地字符串。
10.11 确保释放VM资源
JNI编程时常见的错误之一就是忘记释放VM资源,尤其是在执行路径分支时,比如,有异常发生的时候: JNIEXPORT void JNICALL
Java_pkg_Cls_f(JNIEnv *env, jclass cls, jstring jstr) {
const jchar *cstr =
(*env)->GetStringChars(env, jstr, NULL); if (cstr == NULL) { return; } ...
if (...) { /* exception occurred */
/* misses a ReleaseStringChars call */ return; } ...
/* normal return */
(*env)->ReleaseStringChars(env, jstr, cstr); }
忘记调用ReleaseStringChars可能导致jstring永远被VM给pin着不被回收。一个
GetStringChars必然要对应着一个ReleaseStringChars,下面的代码就没有正确地释放VM资源:
/* The isCopy argument is misused here! */ JNIEXPORT void JNICALL
Java_pkg_Cls_f(JNIEnv *env, jclass cls, jstring jstr) {
jboolean isCopy;
const jchar *cstr = (*env)->GetStringChars(env, jstr, &isCopy); if (cstr == NULL) { return; }
... /* use cstr */
/* This is wrong. Always need to call ReleaseStringChars. */ if (isCopy) {
(*env)->ReleaseStringChars(env, jstr, cstr); } }
即使在isCopy的值是JNI_FALSE时,也应该调用ReleaseStringChars在unpin掉jstring。
10.12 过多的创建局部引用
大量的局部引用创建会浪费不必要的内存。一个局部引用会导致它本身和它所指向的对象都得不到回收。尤其要注意那些长时间运行的方法、创建局部引用的循环和工具函数,充分得利用Pus/PopLocalFrame来高效地管理局部引用。
10.13 使用已经失效的局部引用
局部引用只在一个本地方法的调用期间有效,方法执行完成后会被自动释放。本地代码不应该把存储局部引用存储到全局变量中在其它地方使用。
10.14 跨进程使用JNIEnv
JNIEnv这个指针只能在当前线程中使用,不要在其它线程中使用。
10.15 错误的线程模型(Thread Models)
搞不明白,不翻了。。。
第十一章 JNI设计概述
本章是JNI设计思想的一个概述,在讲的过程中,如果有必要的话,还会对底层实现技术的原理做说明。本章也可以看作是JNIEnv指针、局部和全局引用、字段和方法ID等这些JNI主要技术的规范。有些地方我们可能还会提到一些技术是怎么样去实现的,但我们不会专注于具体的实现方式,主要还是讨论一些实现策略。
11.1 设计目标
JNI最重要的设计目标就是在不同操作系统上的JVM之间提供二进制兼容,做到一个本地库不需要重新编译就可以运行不同的系统的JVM上面。
为了达到这一点儿,JNI设计时不能关心JVM的内部实现,因为JVM的内部实现机制在不断地变,而我们必须保持JNI接口的稳定。
JNI的第二个设计目标就是高效。我们可能会看到,有时为了满足第一个目标,可能需要牺牲一点儿效率,因此,我们需要在平台无关和效率之间做一些选择。
最后,JNI必须是一个完整的体系。它必须提供足够多的JVM功能让本地程序完成一些有用的任务。
JNI不能只针对一款特定的JVM,而是要提供一系列标准的接口让程序员可以把他们的本地代码库加载到不同的JVM中去。有时,调用特定JVM下实现的接口可以提供效率,但更多的情况下,我们需要用更通用的接口来解决问题。
11.2 加载本地库
在JAVA程序可以调用一个本地方法之间,JVM必须先加载一个包含这个本地方法的本地库。
11.2.1 类加载器
本地库通过类加载器定位。类加载器在JVM中有很多用途,如,加载类文件、定义类和接口、提供命令空间机制、定位本地库等。在这里,我们会假设你对类加载器的基本原理已经了解,我们会直接讲述加载器加载和链接类的技术细节。每一个类或者接口都会与最初读取它的class文件并创建类或接口对象的那个类加载器关联起来。只有在名字和定义它们的类加载器都相同的情况下,两个类或者接口的类型才会一致。例如,图11.1中,类加载器L1和L2都定义了一个名字为C的类。这两个类并不相同,因为它们包含了两个不同的f方法,因为它们的f方法返回类型不同。
图11.1 两个名字相同的类被不同类加载器加载的情况
上图中的点划线表达了类加载器之间的关系。一个类加载器必须请求其它类加载器为它加载类或者接口。例如,L1和L2都委托系统类加载器来加载系统类java.lang.String。委托机制,允许不同的类加载器分离系统类。因为L1和L2都委托了系统类加载器来加载系统类,所以被系统类加载器加载的系统类可以在L1和L2之间共享。这种思想很必要,因为如果程序或者系统代码对java.lang.String有不同的理解的话,就会出现类型安全问题。
11.2.2 类加载器和本地库
如图11.2,假设两个C类都有一个方法f。VM使用“C_f”来定位两个C.f方法的本地代码实现。为了确保类C被链接到了正确的本地函数,每一个类加载器都会保存一个与自己相关联的本地库列表。
图11.2 类加载器和本地库的关联
正是由于每一个类加载器都保存着一个本地库列表,所以,只要是被这个类加载器加载的类,都可以使用这个本地库中的本地方法。因此,程序员可以使用一个单一的库来存储所有的本地方法。 当类加载器被回收时,本地库也会被JVM自动被unload。
11.2.3 定位本地库
本地库通过System.loadLibrary方法来加载。下面的例子中,类Cls静态初始化时加载了一个本地库,f方法就是定义在这个库中的。
package pkg; class Cls {
native double f(int i, String s); static {
System.loadLibrary(\ } }
JVM会根据当前系统环境的不同,把库的名字转换成相应的本地库名字。例如,Solaris下,mypkg会被转化成libmypkg.so,而Win32环境下,被转化成mypkg.dll。
JVM在启动的时候,会生成一个本地库的目录列表,这个列表的具体内容依赖于当前的系统环境,比如Win32下,这个列表中会包含Windows系统目录、当前工作目录、PATH环境变量里面的目录。
System.loadLibrary在加载相应本地库失败时,会抛出UnsatisfiedLinkError错误。如果相应的库已经加载过,这个方法不做任何事情。如果底层操作系统不支持动态链接,那么所有的本地方法必须被prelink到VM上,这样的话,VM中调用System.loadLibrary时实际上没有加载任何库。
JVM内部为每一个类加载器都维护了一个已经加载的本地库的列表。它通过三步来决定一个新加载的本地库应该和哪个类加载器关联。