java JNI机制
JNI概述及简单例子介绍
系统环境代指本地操作系统环境,它有自己的本地库和CPU指令集。本地程序(Native Applications)使用C/C++这样的本地语言来编写,被编译成只能在本地系统环境下运行的二进制代码,并和本地库链接在一起。本地程序和本地库一般地会依赖于一个特定的本地系统环境。比如,一个系统下编译出来的C程序不能在另一个系统中运行。
JNI的强大特性使我们在使用JAVA平台的同时,还可以重用原来的本地代码。作为虚拟机实现的一部分,JNI允许JAVA和本地代码间的双向交互。
JNI可以这样与本地程序进行交互:
- 你可以使用JNI来实现"本地方法"(native methods),并在JAVA程序中调用它们。
- JNI支持一个"调用接口"(invocation interface),它允许你把一个JVM嵌入到本地程序中。本地程序可以链接一个实现了JVM的本地库,然后使用"调用接口"执行JAVA语言编写的软件模块。例如,一个用C语言写的浏览器可以在一个嵌入式JVM上面执行从网上下载下来的applets
使用JAVA程序调用C函数打印"Hello World!"。这个过程包含下面几个步骤:
- 创建一个类(HelloWorld.java)声明本地方法。
- 使用javac编译源文件HollowWorld.java,产生HelloWorld.class。使用javah –jni来生成C头文件(HelloWorld.h),这个头文件里面包含了本地方法的函数原型。
- 用C代码写函数原型的实现。
- 把C函数实现编译成一个本地库,创建Hello-World.dll或者libHello-World.so。
- 使用java命令运行HelloWorld程序,类文件HelloWorld.class和本地库(HelloWorld.dll或者libHelloWorld.so)在运行时被加载。
1.创建一个Java类,里面包含着一个native的方法和加载库的方法loadLibrary。HelloNative.java代码如下:
首先大家注意的是native方法,那个加载库的到后面也起作用。native 关键字告诉编译器(其实是JVM)调用的是该方法在外部定义,这里指的是C。
2.运行javah,得到包含该方法的C声明头文件.h
3.根据头文件,写c实现本地方法
这里简单地实现这个sayHello方法如下:
4.生成dll共享库,然后Java程序load库,调用即可
在Windows上,MinGW GCC 运行如下
gcc -m64 -Wl,--add-stdcall-alias -I"C:\Program Files\Java\jdk1.7.0_71\include" -I"C:\Program Files\Java\jdk1.7.0_71\include\include\win32" -shared -o HelloNative.dll HelloNative.c |
-m64表示生成dll库是64位的。然后运行 HelloNative:
java HelloNative
终于成功地可以看到控制台打印如下:
Hello,JNI
JNI设计概述
JNI最重要的设计目标就是在不同操作系统上的JVM之间提供二进制兼容,做到一个本地库不需要重新编译就可以运行不同的系统的JVM上面。为了达到这一点儿,JNI设计时不能关心JVM的内部实现,因为JVM的内部实现机制在不断地变,而我们必须保持JNI接口的稳定。JNI的第二个设计目标就是高效。我们可能会看到,有时为了满足第一个目标,可能需要牺牲一点儿效率,因此,我们需要在平台无关和效率之间做一些选择。最后,JNI必须是一个完整的体系。它必须提供足够多的JVM功能让本地程序完成一些有用的任务。JNI不能只针对一款特定的JVM,而是要提供一系列标准的接口让程序员可以把他们的本地代码库加载到不同的JVM中去。有时,调用特定JVM下实现的接口可以提供效率,但更多的情况下,我们需要用更通用的接口来解决问题。
加载本地库
在JAVA程序可以调用一个本地方法之间,JVM必须先加载一个包含这个本地方法的本地库。
本地库通过类加载器定位。类加载器在JVM中有很多用途,如,加载类文件、定义类和接口、提供命令空间机制、定位本地库等。在这里,我们会假设你对类加载器的基本原理已经了解,我们会直接讲述加载器加载和链接类的技术细节。每一个类或者接口都会与最初读取它的class文件并创建类或接口对象的那个类加载器关联起来。只有在名字和定义它们的类加载器都相同的情况下,两个类或者接口的类型才会一致。下图中类加载器L1和L2都定义了一个名字为C的类。这两个类并不相同,因为它们包含了两个不同的f方法,f方法返回类型不同。
上图中的虚线表达了类加载器之间的关系。一个类加载器必须请求其它类加载器为它加载类或者接口。例如,L1和L2都委托系统类加载器来加载系统类java.lang.String。委托机制,允许不同的类加载器分离系统类。因为L1和L2都委托了系统类加载器来加载系统类,所以被系统类加载器加载的系统类可以在L1和L2之间共享。这种思想很必要,因为如果程序或者系统代码对java.lang.String有不同的理解的话,就会出现类型安全问题。
假设两个C类都有一个方法f。VM使用"C_f"来定位两个C.f方法的本地代码实现。为了确保类C被链接到了正确的本地函数,每一个类加载器都会保存一个与自己相关联的本地库列表。
正是由于每一个类加载器都保存着一个本地库列表,所以,只要是被这个类加载器加载的类,都可以使用这个本地库中的本地方法。因此,程序员可以使用一个单一的库来存储所有的本地方法。当类加载器被回收时,本地库也会被JVM自动被unload。
本地库通过System.loadLibrary方法来加载。下面的例子中,类Cls静态初始化时加载了一个本地库,f方法就是定义在这个库中的。
JVM会根据当前系统环境的不同,把库的名字转换成相应的本地库名字。例如,Solaris下,mypkg会被转化成libmypkg.so,而Win32环境下,被转化成mypkg.dll。
JVM在启动的时候,会生成一个本地库的目录列表,这个列表的具体内容依赖于当前的系统环境,比如Win32下,这个列表中会包含Windows系统目录、当前工作目录、PATH环境变量里面的目录。
System.loadLibrary在加载相应本地库失败时,会抛出UnsatisfiedLinkError错误。如果相应的库已经加载过,这个方法不做任何事情。如果底层操作系统不支持动态链接,那么所有的本地方法必须被prelink到VM上,这样的话,VM中调用System.loadLibrary时实际上没有加载任何库。
JVM内部为每一个类加载器都维护了一个已经加载的本地库的列表。它通过三步来决定一个新加载的本地库应该和哪个类加载器关联。
- 确定System.loadLibrary的调用者。
- 确定定义调用者的类。
- 确定类的加载器。
下面的例子中,JVM会把本地库foo和定义C的类加载器关联起来。
VM中规定,一个JNI本地库只能被一个类加载器加载。当一个JNI本地库已经被第一个类加载器加载后,第二个类加载器再加载时,会报UnsatisfiedLinkError。这样规定的目的是为了确保基于类加载器的命令空间分隔机制在本地库中同样有效。如果不这样的话,通过本地方法进行操作JVM时,很容易造成属于不同类加载器的类和接口的混乱。
一旦JVM回收类加载器,与这个类加载器关联的本地库就会被unload。因为类指向它自己的加载器,所以,这意味着,VM也会被这个类unload。
VM会在第一次使用一个本地方法的时候链接它。假设调用了方法g,而在g的方法体中出现了对方法f的调用,那么本地方法f就会被链接。VM不应该过早地链接本地方法,因为这时候实现这些本地方法的本地库可能还没有被load,从而导致链接错误。
链接一个本地方法需要下面这几个步骤:
- 确定定义了本地方法的类的加载器。
- 在加载器所关联的本地库列表中搜索实现了本地方法的本地函数。
- 建立内部的数据结构,使对本地方法的调用可能直接定向到本地函数。
- VM通过下面这几步,同本地方法的名字生成与之对应的本地函数的名字:
- 前缀"Java_"。
- 类的全名。
- 下划线分隔符"_"。
- 方法名字。
- 有方法重载的情况时,还会有两个下划线("__"),后面跟着参数描述符。
VM在类加载器关联的本地库中搜索符合指定名字的本地函数。对每一个库进行搜索时,VM会先搜索短名字(short name),即没有参数描述符的名字。然后搜索长名字(long name),即有参数描述符的名字。当两个本地方法重载时,程序员需要使用长名字来搜索。但如果一个本地方法和一个非本地方法重载时,就不会使用长名字。
JNI使用一种简单的名字编码协议来确保所有的Unicode字符都被转化成可用的C函数名字。用下划线("_")分隔类的全名中的各部分,取代原来的点(".")。
如果多个本地库中都存在与一个编码后的本地方法名字相匹配的本地函数,哪个本地库首先被加载,则它里面的本地函数就与这个本地方法链接。如果没有哪个函数与给定的本地方法相匹配,则UnsatisfiedLinkError被抛出。
程序员还可以调用JNI函数RegisterNatives来注册与一个类关联的本地方法。这个JNI函数对静态链接函数非常有用。
调用参数
调用转换决定了一个本地函数如何接收参数和返回结果。目前没有一个标准,主要取决于编译器和本地语言的不同。JNI要求同一个系统环境下,调用转换机制必须相同。例如,JNI在UNIX下使用C调用转换,而在Win32下使用stdcall调用转换。如果程序员需要调用的函数遵循不同的调用转换机制,那么最好写一个转换层来解决这个问题。
JNIEnv是一个指向线程局部数据的接口指针,这个指针里面包含了一个指向函数表的指针。在这个表中,每一个函数位于一个预定义的位置上面。JNIEnv很像一个C++虚函数表或者Microsoft COM接口。
线程的局部JNIEnv接口指针
如果一个函数实现了一个本地方法,那么这个函数的第一个参数就是一个JNIEnv接口指针。从同一个线程中调用的本地方法,传入的JNIEnv指针是相同的。本地方法可能被不同的线程调用,这时,传入的JNIEnv指针是不同的。但JNIEnv间接指向的函数表在多个线程间是共享的。JNI指针指向一个线程内的局部数据结构是因为一些平台上面没有对线程局部存储访问的有效支持。因为JNIEnv指针是线程局部的,本地代码决不能跨线程使用JNIEnv。
比起写死一个函数入口来说,使用接口指针可以有以下几个优点:
- JNI函数表是作为参数传递给每一个本地方法的,这样的话,本地库就不必与特定的JVM关联起来。这使得JNI可以在不同的JVM间通用。
- JVM可以提供几个不同的函数表,用于不同的场合。比如,JVM可以提供两个版本的JNI函数表,一个做较多的错误检查,用于调试时;另外一个做较少的错误检查,更高效,用于发布时。
像int、char等这样的基本数据类型,在本地代码和JVM之间进行复制传递,而对象是引用传递的。每一个引用都包含一个指向JVM中相应的对象的指针,但本地代码不能直接使用这个指针,必须通过引用来间接使用。
比起传递直接指针来说,传递引用可以让VM更灵活地管理对象。比如,你在本地代码中抓着一个引用的时候,VM那小子可能这个时候正偷偷摸摸地把这个引用间接指向的那个对象从一块儿内存区域给挪到另一块儿。不过,有一点儿你放心,VM是不敢动对象里面的内容的,因为引用的有效性它要负责。
本地代码中,可以通过JNI创建两种引用,全局引用和局部引用。局部引用的有效期是本地方法的调用期间,调用完成后,局部引用会被JVM自动铲除。而全局引用呢,只要你不手动把它干掉,它会一直站在那里。
JVM中的对象作为参数传递给本地方法时,用的是局部引用。大部分的JNI函数返回局部引用。JNI允许程序员从局部引用创建一个全局引用。接受对象作为参数的JNI函数既支持全局引用也支持局部引用。本地方法执行完毕后,向JVM返回结果时,它可能向JVM返回局部引用,也可能返回全局引用。
局部引用只在创建它的线程内部有效。本地代码不能跨线程传递和使用局部引用。
JNI中的NULL引用指向JVM中的null对象。对一个全局引用或者局部引用来说,只要它的值不是NULL,它就不会指向一个null对象。
一个对象从JVM传递给本地方法时,就把控制权移交了过去,JVM会为每一个对象的传递创建一条记录,一条记录就是一个本地代码中的引用和JVM中的对象的一个映射。记录中的对象不会被GC回收。所有传递到本地代码中的对象和从JNI函数返回的对象都被自动地添加到映射表中。当本地方法返回时,VM会删除这些映射,允许GC回收记录中的数据。下图演示了局部引用记录是怎么样被创建和删除的。一个JVM窗口对应一个本地方法,窗口里面包含了一个指向局部引用映射表的指针。方法D.f调用本地方法C.g。C.g通过C函数Java_C_g来实现。在进入到Java_C_g之前,虚拟机会创建一个局部引用映射表,当Java_C_g返回时,VM会删掉这个局部引用映射表。
有许多方式可以实现一个映射表,比如栈、表、链表、哈希表。实现时可能会使用引用计数来避免重得。
JNI允许本地代码通过名字和类型描述符来访问JAVA中的字段或调用JAVA中的方法。例如,为了读取类cls中的一个int实例字段,本地方法首先要获取字段ID:
jfieldID fid = env->GetFieldID(env, cls, "i", "I");
然后可以多次使用这个ID,不需要再次查找:
jint value = env->GetIntField(env, obj, fid);
除非JVM把定义这个字段和方法的类或者接口unload,字段ID和方法ID会一直有效。字段和方法可以来自定个类或接口,也可以来自它们的父类或间接父类。JVM规范规定:如果两个类或者接口定义了相同的字段和方法,那么它们返回的字段ID和方法ID也一定会相同。例如,如果类B定义了字段fld,类C从B继承了字段fld,那么程序从这两个类上获取到的名字为"fld"的字段的字段ID是相同的。JNI不会规定字段ID和方法ID在JVM内部如何实现。通过JNI,程序只能访问那些已经知道名字和类型的字段和方法。而使用Java Core Reflection机制提供的API,程序员不用知道具体的信息就可以访问字段或者调用方法。有时在本地代码中调用反射机制也很有用。所以,JDK提供了一组API来在JNI字段ID和java.lang.reflect.Field类的实例之间转换,另外一组在JNI方法ID和java.lang.reflect.Method类实例之间转换。