系统环境代指本地操做系统环境,它有本身的本地库和CPU指令集。本地程序(Native Applications)使用C/C++这样的本地语言来编写,被编译成只能在本地系统环境下运行的二进制代码,并和本地库连接在一块儿。本地程序和本地库通常地会依赖于一个特定的本地系统环境。好比,一个系统下编译出来的C程序不能在另外一个系统中运行。java
JNI的强大特性使咱们在使用JAVA平台的同时,还能够重用原来的本地代码。做为虚拟机实现的一部分,JNI容许JAVA和本地代码间的双向交互。程序员
JNI能够这样与本地程序进行交互:浏览器
使用JAVA程序调用C函数打印"Hello World!"。这个过程包含下面几个步骤:安全
1.建立一个Java类,里面包含着一个native的方法和加载库的方法loadLibrary。HelloNative.java代码以下:数据结构
首先你们注意的是native方法,那个加载库的到后面也起做用。native 关键字告诉编译器(实际上是JVM)调用的是该方法在外部定义,这里指的是C。app
2.运行javah,获得包含该方法的C声明头文件.h 函数
3.根据头文件,写c实现本地方法 编码
这里简单地实现这个sayHello方法以下:spa
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最重要的设计目标就是在不一样操做系统上的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内部为每个类加载器都维护了一个已经加载的本地库的列表。它经过三步来决定一个新加载的本地库应该和哪一个类加载器关联。
下面的例子中,JVM会把本地库foo和定义C的类加载器关联起来。
VM中规定,一个JNI本地库只能被一个类加载器加载。当一个JNI本地库已经被第一个类加载器加载后,第二个类加载器再加载时,会报UnsatisfiedLinkError。这样规定的目的是为了确保基于类加载器的命令空间分隔机制在本地库中一样有效。若是不这样的话,经过本地方法进行操做JVM时,很容易形成属于不一样类加载器的类和接口的混乱。
一旦JVM回收类加载器,与这个类加载器关联的本地库就会被unload。由于类指向它本身的加载器,因此,这意味着,VM也会被这个类unload。
VM会在第一次使用一个本地方法的时候连接它。假设调用了方法g,而在g的方法体中出现了对方法f的调用,那么本地方法f就会被连接。VM不该该过早地连接本地方法,由于这时候实现这些本地方法的本地库可能尚未被load,从而致使连接错误。
连接一个本地方法须要下面这几个步骤:
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。
比起写死一个函数入口来讲,使用接口指针能够有如下几个优势:
像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类实例之间转换。