Java JNI机制

java JNI机制

JNI概述及简单例子介绍

系统环境代指本地操作系统环境,它有自己的本地库和CPU指令集。本地程序(Native Applications)使用C/C++这样的本地语言来编写,被编译成只能在本地系统环境下运行的二进制代码,并和本地库链接在一起。本地程序和本地库一般地会依赖于一个特定的本地系统环境。比如,一个系统下编译出来的C程序不能在另一个系统中运行。

????JNI的强大特性使我们在使用JAVA平台的同时,还可以重用原来的本地代码。作为虚拟机实现的一部分,JNI允许JAVA和本地代码间的双向交互。

JNI可以这样与本地程序进行交互:

  1. 你可以使用JNI来实现"本地方法"(native methods),并在JAVA程序中调用它们。
  2. JNI支持一个"调用接口"(invocation interface),它允许你把一个JVM嵌入到本地程序中。本地程序可以链接一个实现了JVM的本地库,然后使用"调用接口"执行JAVA语言编写的软件模块。例如,一个用C语言写的浏览器可以在一个嵌入式JVM上面执行从网上下载下来的applets

使用JAVA程序调用C函数打印"Hello World!"。这个过程包含下面几个步骤:

  1. 创建一个类(HelloWorld.java)声明本地方法。
  2. 使用javac编译源文件HollowWorld.java,产生HelloWorld.class。使用javah –jni来生成C头文件(HelloWorld.h),这个头文件里面包含了本地方法的函数原型。
  3. 用C代码写函数原型的实现。
  4. 把C函数实现编译成一个本地库,创建Hello-World.dll或者libHello-World.so。
  5. 使用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内部为每一个类加载器都维护了一个已经加载的本地库的列表。它通过三步来决定一个新加载的本地库应该和哪个类加载器关联。

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

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

VM中规定,一个JNI本地库只能被一个类加载器加载。当一个JNI本地库已经被第一个类加载器加载后,第二个类加载器再加载时,会报UnsatisfiedLinkError。这样规定的目的是为了确保基于类加载器的命令空间分隔机制在本地库中同样有效。如果不这样的话,通过本地方法进行操作JVM时,很容易造成属于不同类加载器的类和接口的混乱。

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

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

链接一个本地方法需要下面这几个步骤:

  1. 确定定义了本地方法的类的加载器。
  2. 在加载器所关联的本地库列表中搜索实现了本地方法的本地函数。
  3. 建立内部的数据结构,使对本地方法的调用可能直接定向到本地函数。
  4. VM通过下面这几步,同本地方法的名字生成与之对应的本地函数的名字:
  5. 前缀"Java_"。
  6. 类的全名。
  7. 下划线分隔符"_"。
  8. 方法名字。
  9. 有方法重载的情况时,还会有两个下划线("__"),后面跟着参数描述符。

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。

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

  1. JNI函数表是作为参数传递给每一个本地方法的,这样的话,本地库就不必与特定的JVM关联起来。这使得JNI可以在不同的JVM间通用。
  2. 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类实例之间转换。

附:类型映射

原文地址:https://www.cnblogs.com/kexinxin/p/11689641.html

时间: 2024-10-27 14:02:16

Java JNI机制的相关文章

深入浅出 - Android系统移植与平台开发(十二)- Android JNI机制

第五章.JNI机制 4.1 JNI概述 由前面基础知识可知,Android的应用层由Java语言编写,Framework框架层则是由Java代码与C/C++语言实现,之所以由两种不同的语言组合开发框架层,是由于Java代码是与硬件环境彻底"隔离"的跨平台语言,Java代码无法直接操作硬件. 比方:Android系统支持大量传感器.Java运行在虚拟机中,无法直接得到传感器数据.而Android系统基于Linux操作系统.在Linux操作系统中C/C++通过Linux提供的系统调用接口能

编程基础知识——Java JNI开发流程(2)

android中使用jni调用本地C++库 android平台上的本地库文件后缀 .so.类似windows上的dll文件. 要在android上使用jni,首先需要下载android ndk. 操作步骤,正常建立android工程,然后在android工程那里右键,属性,选择Android Tools -> Add Native Support.就可以为android工程增加本地库支持. 添加支持后的android工程,会增加jni目录,C++代码就写在这个目录里. 新建一个类,并且使用nat

Java GC机制和对象Finalize方法的一点总结

GC是什么? 为什么要有GC? GC是垃圾收集的意思(Garbage Collection),内存处理是编程人员容易出现问题的地方,忘记或者错误的内存回收会导致程序或系统的不稳定甚至崩溃,Java提供的GC功能可以自动监测对象是否超过作用域从而达到自动回收内存的目的. 有向图垃圾回收机制 .NET的垃圾回收采用引用计数,java的垃圾回收机制采取的是有向图的方式来实现,具体的说,java程序中的每个线程对象就可以看作是一个有向图的起点,有向边从栈中的引用者指向堆中的引用对象.在这个有向图中,如果

【深入Java虚拟机】之二:Java垃圾收集机制

[深入Java虚拟机]之:Java垃圾收集机制 对象引用 Java中的垃圾回收一般是在Java堆中进行,因为堆中几乎存放了Java中所有的对象实例.谈到Java堆中的垃圾回收,自然要谈到引用.在JDK1.2之前,Java中的引用定义很很纯粹:如果reference类型的数据中存储的数值代表的是另外一块内存的起始地址,就称这块内存代表着一个引用.但在JDK1.2之后,Java对引用的概念进行了扩充,将其分为强引用(Strong Reference).软引用(Soft Reference).弱引用(

JAVA垃圾收集机制剖析

1.垃圾收集算法的核心思想 Java语言建立了垃圾收集机制,用以跟踪正在使用的对象和发现并回收不再使用(引用)的对象.该机制可以有效防范动态内存分配中可能发生的两个危险:因内存垃圾过多而引发的内存耗尽,以及不恰当的内存释放所造成的内存非法引用. 垃圾收集算法的核心思想是:对虚拟机可用内存空间,即堆空间中的对象进行识别,如果对象正在被引用,那么称其为存活对象,反之,如果对象不再被引用,则为垃圾对象,可以回收其占据的空间,用于再分配.垃圾收集算法的选择和垃圾收集系统参数的合理调节直接影响着系统性能,

Java JNI的详细介绍

JNI就是Java Native Interface的简称,也就是java本地接口.它提供了若干的API实现了和Java和其他语言的通信(主要是C&C++).或许不少人觉得Java已经足够强大,为什么要需要JNI这种东西呢?我们知道Java是一种平台无关性的语言,平台对于上层的java代码来说是透明的,所以在多数时间我们是不需要JNI的,但是假如你遇到了如下的三种情况之一呢? 你的Java代码,需要得到一个文件的属性.但是你找遍了JDK帮助文档也找不到相关的API. 在本地还有一个别的系统,不过

Android平台Native开发与JNI机制详解

源文链接: http://mysuperbaby.iteye.com/blog/915425 一个Native Method就是一个Java调用非Java代码的接口.一个Native Method是这样一个Java的方法:该方法的实现由非Java语言实现,比如C或C++. 个人认为下面这篇转载的文章写的很清晰很不错. 注意Android平台上的JNI机制使用包括Java代码中调用Native模块以及Native代码中调用Java模块. http://www.ophonesdn.com/artic

Java回收机制总结

Java回收机制 如何确定"垃圾" 既然是垃圾回收机制,第一步肯定是要确定垃圾,知道了垃圾便可以进行回收.但是如何确定垃圾呢?什么是垃圾呢? 什么是"垃圾" 首先要明白什么是"垃圾",垃圾回收机制是回收堆内存中的对象(具体的内存划分可以看:),对于栈中的对象是不需要回收机制去考虑的.在Java中堆内存中的对象是通过和栈内存中的引用相互关联,才被利用的.既然是对堆内存的回收,并且堆内存中存储的都是引用对象的实体,所以回收的就是没有被任何一个引用所关

java反射机制(一)—— 利用反射机制实例化对象

一.Java有着一个非常突出的动态相关机制:Reflection,用在Java身上指的是我们可以于运行时加载.探知.使用编译期间完全未知的classes.换句话说,Java程序可以加载一个运行时才得知名称的class,获悉其完整构造(但不包括methods定义),并生成其对象实体.或对其fields设值.或唤起其methods.(度娘文库是这么说的) 二.这篇文章主要介绍一下通过反射机制去实例化一个类的对象,然后调用其方法.本文主要介绍两种方式,第一种就是通过构造函数来实例化,第二种就是通过Cl