jni的概念,jni详解
JAVA中用JNI调用了在WIN32下编译的DLL,如果在LINUX下执行这个JAVA程序,是否还可以继续调用的DLL。
直接是不行的! linux里面没有dll。
不过看你的需求应该是java调用了其他程序别写的功能,在linux里面肯定可以!Linux也有动态链接库的概念,不过叫Share Object ,你编译一个.so的文件,然后还是使用JNI调用。
参考:
编译so:
java调用:
C调用java时使用JNI的问题!急。。。。。
M,是其中的一个组成部分,更详细的看下面:
----------------------------------
操作系统装入jvm是通过jdk中java.exe来完成,通过下面4步来完成jvm环境.
1.创建jvm装载环境和配置
2.装载jvm.dll
3.初始化jvm.dll并挂界到JNIENV(JNI调用接口)实例
4.调用JNIEnv实例装载并处理class类。
在我们运行和调试java程序的时候,经常会提到一个jvm的概念.jvm是java程序运行的环境,但是他同时一个操作系统的一个应用程序一个进程,因此他也有他自己的运行的生命周期,也有自己的代码和数据空间.
首先来说一下jdk这个东西,不管你是初学者还是高手,是j2ee程序员还是j2se程序员,jdk总是在帮我们做一些事情.我们在了解java之前首先大师们会给我们提供说jdk这个东西.它在java整个体系中充当着什么角色呢?我很惊叹sun大师们设计天才,能把一个如此完整的体系结构化的如此完美.jdk在这个体系中充当一个生产加工中心,产生所有的数据输出,是所有指令和战略的执行中心.本身它提供了java的完整方案,可以开发目前java能支持的所有应用和系统程序.这里说一个问题,大家会问,那为什么还有j2me,j2ee这些东西,这两个东西目的很简单,分别用来简化各自领域内的开发和构建过程.jdk除了jvm之外,还有一些核心的API,集成API,用户工具,开发技术,开发工具和API等组成
好了,废话说了那么多,来点于主题相关的东西吧.jvm在整个jdk中处于最底层,负责于操作系统的交互,用来屏蔽操作系统环境,提供一个完整的java运行环境,因此也就虚拟计算机. 操作系统装入jvm是通过jdk中java.exe来完成,通过下面4步来完成jvm环境.
1.创建jvm装载环境和配置
2.装载jvm.dll
3.初始化jvm.dll并挂界到JNIENV(JNI调用接口)实例
4.调用JNIEnv实例装载并处理class类。
一.jvm装入环境,jvm提供的方式是操作系统的动态连接文件.既然是文件那就一个装入路径的问题,java是怎么找这个路径的呢?当你在调用java test的时候,操作系统会在path下在你的java.exe程序,java.exe就通过下面一个过程来确定jvm的路径和相关的参数配置了.下面基于windows的实现的分析.
首先查找jre路径,java是通过GetApplicationHome api来获得当前的java.exe绝对路径,c:\j2sdk1.4.2_09\bin\java.exe,那么它会截取到绝对路径c:\j2sdk1.4.2_09\,判断c:\j2sdk1.4.2_09\bin\java.dll文件是否存在,如果存在就把c:\j2sdk1.4.2_09\作为jre路径,如果不存在则判断c:\j2sdk1.4.2_09\jre\bin\java.dll是否存在,如果存在这c:\j2sdk1.4.2_09\jre作为jre路径.如果不存在调用GetPublicJREHome查HKEY_LOCAL_MACHINE\Software\JavaSoft\Java Runtime Environment\“当前JRE版本号”\JavaHome的路径为jre路径。
然后装载jvm.cfg文件JRE路径+\lib+\ARCH(CPU构架)+\jvm.cfgARCH(CPU构架)的判断是通过java_md.c中GetArch函数判断的,该函数中windows平台只有两种情况:WIN64的‘ia64’,其他情况都为‘i386’。以我的为例:C:\j2sdk1.4.2_09\jre\lib\i386\jvm.cfg.主要的内容如下:
-client KNOWN
-server KNOWN
-hotspot ALIASED_TO -client
-classic WARN
-native ERROR
-green ERROR
在我们的jdk目录中jre\bin\server和jre\bin\client都有jvm.dll文件存在,而java正是通过jvm.cfg配置文件来管理这些不同版本的jvm.dll的.通过文件我们可以定义目前jdk中支持那些jvm,前面部分(client)是jvm名称,后面是参数,KNOWN表示jvm存在,ALIASED_TO表示给别的jvm取一个别名,WARN表示不存在时找一个jvm替代,ERROR表示不存在抛出异常.在运行java XXX是,java.exe会通过CheckJvmType来检查当前的jvm类型,java可以通过两种参数的方式来指定具体的jvm类型,一种按照jvm.cfg文件中的jvm名称指定,第二种方法是直接指定,它们执行的方法分别是“java -J”、“java -XXaltjvm=”或“java -J-XXaltjvm=”。如果是第一种参数传递方式,CheckJvmType函数会取参数‘-J’后面的jvm名称,然后从已知的jvm配置参数中查找如果找到同名的则去掉该jvm名称前的‘-’直接返回该值;而第二种方法,会直接返回“-XXaltjvm=”或“-J-XXaltjvm=”后面的jvm类型名称;如果在运行java时未指定上面两种方法中的任一一种参数,CheckJvmType会取配置文件中第一个配置中的jvm名称,去掉名称前面的‘-’返回该值。CheckJvmType函数的这个返回值会在下面的函数中汇同jre路径组合成jvm.dll的绝对路径。如果没有指定这会使用jvm.cfg中第一个定义的jvm.可以通过set _JAVA_LAUNCHER_DEBUG=1在控制台上测试.
最后获得jvm.dll的路径,JRE路径+\bin+\jvm类型字符串+\jvm.dll就是jvm的文件路径了,但是如果在调用java程序时用-XXaltjvm=参数指定的路径path,就直接用path+\jvm.dll文件做为jvm.dll的文件路径.
二:装载jvm.dll
通过第一步已经找到了jvm的路径,java通过LoadJavaVM来装入jvm.dll文件.装入工作很简单就是调用windows API函数:
LoadLibrary装载jvm.dll动态连接库.然后把jvm.dll中的导出函数JNI_CreateJavaVM和JNI_GetDefaultJavaVMInitArgs挂接到InvocationFunctions变量的CreateJavaVM和GetDefaultJavaVMInitArgs函数指针变量上。jvm.dll的装载工作宣告完成。
三:初始化jvm,获得本地调用接口,这样就可以在java中调用jvm的函数了.调用InvocationFunctions-CreateJavaVM也就是jvm中JNI_CreateJavaVM方法获得JNIEnv结构的实例.
四:运行java程序.
java程序有两种方式一种是jar包,一种是class. 运行jar,java -jar XXX.jar运行的时候,java.exe调用GetMainClassName函数,该函数先获得JNIEnv实例然后调用java类java.util.jar.JarFileJNIEnv中方法getManifest()并从返回的Manifest对象中取getAttributes("Main-Class")的值即jar包中文件:META-INF/MANIFEST.MF指定的Main-Class的主类名作为运行的主类。之后main函数会调用java.c中LoadClass方法装载该主类(使用JNIEnv实例的FindClass)。main函数直接调用java.c中LoadClass方法装载该类。如果是执行class方法。main函数直接调用java.c中LoadClass方法装载该类。
然后main函数调用JNIEnv实例的GetStaticMethodID方法查找装载的class主类中
“public static void main(String[] args)”方法,并判断该方法是否为public方法,然后调用JNIEnv实例的
CallStaticVoidMethod方法调用该java类的main方法。
另外,虚机团上产品团购,超级便宜
英语Max Non Heap Memory怎么翻译?
为什么要学习JVM?
深入理解JVM可以帮助我们从平台角度提高解决问题的能力,例如,有效防止内存泄漏(Memory leak),优化线程锁的使用 (Thread Lock),更加高效的进行垃圾回收 (Garbage collection),提高系统吞吐量 (throughput),降低延迟(Delay),提高其性能(performance)等。
你是如何理解JVM的?
JVM 是 Java Virtual Machine的缩写,顾名思义,它是一个虚拟计算机,是硬件计算机的抽象(虚构)实现,是JAVA平台的一部分,如图所示(见图中的最底端):
JVM是 Java 程序能够实现跨平台的基础(Java的跨平台本质上是通过不同平台的JVM实现的),它的作用是加载 Java 程序,把字节码(bytecode)翻译成机器码再交由 CPU 执行。如图所示:
程序在执行之前先要把 Java 代码(.java)转换成字节码(.class),JVM 通过类加载器(ClassLoader)把字节码加载到内存中,【关注尚硅谷,轻松学IT】但字节码文件是 JVM 的一套指令集规范,并不能直接交给底层操作系统去执行,因此需要特定的命令解析器执行引擎(Execution Engine) 将字节码翻译成底层机器码,再交由 CPU 去执行。
市场上有哪些主流的JVM呢?
JVM是一种规范,基于这种规范,不同公司做了具体实现,BEA公司研发JRockit VM?,后在2008年由Oracle公司收购;IBM公司研发了J9 VM?,只应用于IBM 内部。Sun公司研发了HotSpot VM?,后在2010年由Oracle公司收购。目前是甲骨文公司最主流的一款JVM虚拟机,也是我们现在最常用的一种。
JVM的体系结构是怎样的?
JVM 的体系结构,如图所示:
类加载系统 (ClassLoader System)负责加载类到内存;运行时数据区 (Runtime Data Area)负责存储对象数据信息;执行引擎(Execution Engine)负责调用对象执行业务;本地库接口(Native Interface)负责融合不同的编程语言为 Java 所用。
JVM有哪些运行模式吗?
JVM有两种运行模式Server与Client。两种模式的区别在于,Client模式启动速度较快,Server模式启动较慢;但是启动进入稳定期之后Server模式的程序运行速度比Client要快很多。这是因为Server模式启动的JVM采用的是重量级的虚拟机,对程序采用了更多的优化;而Client模式启动的JVM采用的是轻量级的虚拟机。所以Server启动慢,但稳定后速度比Client远远要快。
现在64位的jdk中默认都是server模式(可通过 java -version进行查看)。当虚拟机运行在-client模式的时候,使用的是一个代号为C1的轻量级编译器, 而server模式启动的虚拟机采用相对重量级,代号为C2的编译器.c1、c2都是JIT编译器, C2比C1编译器编译的相对彻底,服务起来之后,性能更高。
JVM 运行时内存结构是怎样的?
不同虚拟机实现可能略微有所不同,但都会遵从 Java 虚拟机规范,Java 8 虚
拟机规范规定,Java 虚拟机所管理的内存将会包括以下几个区域,如图所示:
Java 堆(Heap)
Java堆(Java Heap)是 JVM 中内存最大的一块,被所有线程共享的,在虚拟机启动时创建,主要用于存放对象实例,大部分对象实例也都是在这里分配。随着JIT编译器的发展和逃逸分析技术的逐渐成熟,栈上分配、标量替换优化的技术将会导致一些微妙的变化,所有的对象都分配在堆上渐渐变得不那么绝对了。小对象未逃逸还可以在直接在栈上分配。如果在堆中没有内存完成实例分配,并且堆已不可以再进行扩展时,系统底层运行时将会抛出 OutOfMemoryError。Java 虚拟机规范规定,Java 堆可以处在物理上不连续的内存空间中,只要逻辑上连续即可,就像我们的磁盘空间一样。在实现上也可以是固定大小的,也可以是可扩展的,不过当前主流的虚拟机都是可扩展的,通过 -Xmx 和 -Xms 参数定义堆内存大小。
方法区(Method Area)
方法区(Methed Area)是一种规范,用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译后的代码等数据。不同jdk,方法区的实现不同,HotSpot 虚拟机在 JDK 8 中使用 Native Memory 来实现方法区。当方法无法满足内存分配需求时会抛出 OutOfMemoryError 异常。
Java 虚拟机栈(VM Stack)
Java 虚拟机栈(Java Virtual Machine Stacks)描述的是 Java 方法执行时的内存模型,每个方法在被线程调用时都会创建一个栈帧(Stack Frame)用于存储局部变量表、操作数栈、动态链接、方法出口等信息,每个方法从调用直至执行完成的过程,【关注尚硅谷,轻松学IT】都对应着一个栈帧在虚拟机栈中入栈到出栈的过程。如果线程请求的栈深度大于虚拟机所允许的栈深度就会抛出 StackOverflowError 异常。如果虚拟机是可以动态扩展的,如果扩展时无法申请到足够的内存就会抛出 OutOfMemoryError 异常。
JVM本地方法栈 (Native Method Stack)
本地方法栈(Native Method Stack)与虚拟机栈的作用类似,只不过虚拟机栈是服务 Java 方法的,而本地方法栈是为虚拟机调用 Native 方法服务的。在 Java 虚拟机规范中对于本地方法栈没有特殊的要求,虚拟机可以自由的实现它,因此在 Sun HotSpot 虚拟机直接把本地方法栈和虚拟机栈合二为一了。
JVM程序计数器(Program Counter Register)
程序计数器(Program Counter Register)是一块较小的内存空间,它可以看作是当前线程执行的字节码的行号指示器。在虚拟机的概念模型里,字节码解析器的工作是通过改变这个计数器的值来选取下一条需要执行的字节码指令,分支、循环、跳转、异常处理、线程恢复等基础功能都需要依赖这个计数器来完成。
由于 JVM 的多线程是通过线程轮流切换并分配处理器执行时间的方式来实现的,也就是任何时刻,一个处理器(或者说一个内核)都只会执行一条线程中的指令。因此为了线程切换后能恢复到正确的执行位置,每个线程都有独立的程序计数器。
如果线程正在执行 Java 中的方法,程序计数器记录的就是正在执行虚拟机字节码指令的地址,如果是 Native 方法,这个计数器就为空(undefined),因此该内存区域是唯一一个在 Java 虚拟机规范中没有规定 OutOfMemoryError 的区域。
如何理解JVM中的GC系统?
追踪仍然使用的所有对象,并将其余对象标记为垃圾,然后进行回收,这个过程称之为GC(垃圾回收).所有的GC系统可从GC判断策略(例如引用计数,对象可达性分析),GC收集算法(标记-清除,标记-清除-整理,标记-复制-清除,分代),GC收集器(例如Serial,Parallel,CMS,G1)等方面进行学习
JVM引用链中可以作为 Root 的对象?
Java 虚拟机栈中的引用对象;
本地方法栈中 JNI(既一般说的 Native 方法)引用的对象;
方法区中类静态常量的引用对象;
方法区中常量的引用对象。
JVM中常见垃圾回收算法有哪些?
引用计数器算法
这个算法是给每一个对象设置一个引用计数器,每当有一个地方引用这个对象的时候,计数器就加 1,与之相反,每当引用失效的时候就减 1。也就是以计数来判断对象是否为垃圾。例如:
引用计数法,有一个很大的缺陷就是循环引用,例如:
可达性分析算法
这个算法的核心思路就是通过一系列的“GC Roots”对象作为起始点,从这些对象开始往下搜索,搜索所经过的路径称之为“引用链”。当一个对象到 GC Roots 没有任何引用链相连的时候,证明此对象是可以被回收的。例如:
复制算法
这个算法是将内存分为大小相同的两块,当这一块使用完了,就把当前存活的对象复制到另一块,然后一次性清空当前区块。此算法的缺点是只能利用一半的内存空间。例如:
标记-清除算法
这个算法执行分两阶段,第一阶段从引用根节点开始标记所有被引用的对象,第二阶段遍历整个堆,把未标记的对象清除。此算法需要暂停整个应用,同时,会产生内存碎片。例如:
标记-整理算法
这个算法结合了“标记-清除”和“复制”两个算法的优点。第一阶段从根节点开始标记所有被引用对象,第二阶段遍历整个堆,把存活对象“压缩”复制到堆的其中一块空间中,按顺序排放。此算法避免了“标记-清除”的碎片问题,同时也避免了“复制”算法的空间问题,例如:
JVM对象引用都有哪些类型?
不管是引用计数法还是可达性分析算法都与对象的“引用”有关[说说Java中的四大引用类型。],这说明对象的引用决定了对象的生死,对象的引用关系如下。
强引用
在代码中普遍存在的,类似 Object obj = new Object() 这类引用,只要强引用还在,垃圾收集器永远不会回收掉被引用的对象。
软引用
是一种相对强引用弱化一些的引用,可以让对象豁免一些垃圾收集,只有当JVM 认为内存不足时,才会去试图回收软引用指向的对象,JVM 会确保在抛出 OutOfMemoryError 之前,清理软引用指向的对象。
弱引用
非必需对象,但它的强度比软引用更弱,被弱引用关联的对象只能生存到下一次垃圾收集发生之前。
虚引用
也称为幽灵引用或幻影引用,是最弱的一种引用关系,无法通过虚引用来获取一个对象实例,为对象设置虚引用的目的只有一个,就是当这个对象被收集器回收时收到一条系统通知。
JVM垃圾回收器的分类都有哪些?
新生代回收器
Serial、ParNew、Parallel Scavenge
老年代回收器
Serial Old、Parallel Old、CMS
整堆回收器
G1垃圾回收器
分代垃圾回收器的组成部分有哪些?
分代垃圾回收器是由新生代(Young Generation)和老生代(Tenured Generation)组成的,默认情况下新生代和老生代的内存比例是 1:2。
新生代的组成部分有哪些?:
新生代是由:Eden、Form Survivor、To Survivor 三个区域组成的,它们内存默认占比是 8:1:1,如图所示:
新生代垃圾回收是怎么执行的?
第一步将Eden和From Survivor 活着的对象复制到 To Survivor 区;第二步将清空 Eden 和 From Survivor 分区;第三步将From Survivor 和 To Survivor 分区交换(From 变 To,To 变 From)。当新生代的 Survivor 分区为 2 个的时候,不论是空间利用率还是程序运行的效率都是最优的。
谈谈JVM中的CMS 垃圾回收器?
CMS(Concurrent Mark and Sweep)是并发标记和清除垃圾收集器。它会使用空闲列表(free-lists)管理内存空间的回收,不对老年代进行整理。其优点是在标记、清除阶段的大部分工作和应用线程一起并发执行。可以降低延迟,缩短停顿时间,提高服务的响应时间。当然也有缺陷,主要表现在,对 CPU 资源要求敏感,无法清除浮动垃圾(浮动垃圾指的是 CMS 清除垃圾的时候,还有用户线程产生新的垃圾,这部分未被标记的垃圾叫做“浮动垃圾”,只能在下次 GC 的时候进行清除),还会产生大量空间碎片。
谈谈JVM中的是 G1 垃圾回收器?
G1(Garbage-First GC)是一款实时收集器,其设计目标是将STW停顿时间和分布变成可预期以及可配置的。可以说是一种兼顾吞吐量和停顿时间的 GC 实现。G1 可以直观的设定停顿时间的目标,相比于 CMS ,G1 未必能做到 CMS 在最好情况下的延时停顿,但是最差情况要好很多。
使用G1收集器时,Java堆的内存布局与其他收集器有很大差别,它将整个Java堆划分为多个大小相等的独立区域(Region),虽然还保留有新生代和老年代的概念,但新生代和老年代不再是物理隔阂了,它们都是一部分(可以不连续)Region的集合,例如:
这样的划分使得 GC不必每次都去收集整个堆空间, 而是以增量的方式来处理,每次只处理一部分小堆区,称为此次的回收集(collection set). 每次暂停都会收集所有年轻代的小堆区, 同时也可能只包含一部分老年代小堆区。
G1的另一项创新, 是在并发阶段估算每个小堆区存活对象的总数。用来构建回收集(collection set)的原则是: 垃圾最多的小堆区会被优先收集。这也是G1名称的由来:garbage-first。
G1 解决了 CMS 中的各种疑难问题, 包括暂停时间的可预测性, 并终结了堆内存的碎片化。对单业务延迟非常敏感的系统来说, 如果CPU资源不受限制,那么G1可以说是 HotSpot 中最好的选择, 特别是在最新版本的Java虚拟机中。当然,这种降低延迟的优化也不是没有代价的: 由于额外的写屏障(write barriers)和更积极的守护线程, G1的开销会更大。所以, 如果系统属于吞吐量优先型的,又或者CPU持续占用100%, 而又不在乎单次GC的暂停时间, 那么CMS是更好的选择。
JVM垃圾回收的调优参数有哪些?
-Xmx:512 设置最大堆内存为 512 M;
-Xms:256 初始堆内存(最小堆)为 256 M;
-XX:MaxNewSize 设置最大年轻代内存;
-XX:MaxTenuringThreshold=6 设置新生代对象经过6次GC晋升到老年代;
-XX:PretrnureSizeThreshold 设置大对象的值,超过这个值的大对象直接进入老生代;
-XX:NewRatio 设置分代垃圾回收器新生代和老生代内存占比;
-XX:SurvivorRatio 设置新生代 Eden、Form Survivor、To Survivor 占比。
JVM现代并发GC有什么调优原则
第一要空间换时间与效率,针对G1 ZGC 加大堆内存(更多的空余空间)的配置往往更有利 于GC达到目标暂停时间。第二要知道低暂停不代表高吞吐量,并发GC是保证并发阶段GC的同时业务线程依然有几率获得CPU时间片,但同时也意味着GC会与业务线程抢占计算资源,且往往更多的并发阶段为了处理更多的同步问题,也会占用更多的计算资源。第三是GC调优永远要考虑机器资源,对应系统应用场景等等,至少目前没有银弹。
文章来源于jason
如何编写一个界面程序,控制另一个程序的运行?
交互式应用程序,有很多分支和细节,在Windows下的COM组件类型开发,UNIX
xopen,
JAVA
JNI用于程序交互编程思想,更早的命令行接口,VBX等,可以用于程序交互。关键是做什么,用什么方便的问题。
一、如果是在Windows下,最简单的交互模式是一个命令行,第二个是新闻之间的过程,又是一个全局钩子(这是拦截不控制),但更彻底,建议理解COM
+的概念和方法(这至少要读一本书),这是最基本的东西赢下,组件的基本理论,控件、插件。
二、这一要求,控制VS打开文件,使用命令行,但是为了实现内部跳转和其他控制“动作”,至少您想要开发一个v插件来匹配接口程序。这一要求,您应该使用进程间消息不解决,因为消息之间的过程属于弱控制,如果双方都是我自己的开发好,而第三方软件合作,更麻烦。
怎么回收jni中的global
JNI规范中定义了三种引用——全局引用(Global reference),局部引用(Local reference),弱全局引用(Weak global reference)。
这算三种引用的生存期是不同的。
全局引用的生存期为创建之后,直到程序员显式的释放它。
局部引用的生存期为创建后,直到程序员显式的释放他们,或在当前上下文(可以理解成Java程序调用Native代码的过程)结束之后没有被JVM发现有JAVA层引用而被JVM回收并释放。
弱全局引用的生存期为创建之后,到程序员显式的释放他们或JVM认为应该回收它的时候(比如内存紧张的时候)进行回收而被释放。
二、局部引用
这里重点要强调一下局部引用的有效期,很多有C语言背景的程序员会认为当Native函数结束之后局部引用就无效了,和C语言的局部变量对应。实际上JNI中的局部引用和C语言中局部变量是不同的,它的有效期不只是当前Native函数被调用的上下文中。我理解的调用上下文,为Java虚拟机的调用流程。Native函数是被Java虚拟机调用的,Native函数执行完成之后,控制流程将继续返回给Java虚拟机。局部变量在Native函数中,由Native代码调用Java虚拟机的JNI接口创建,秉着谁创建谁销毁的原则(软件设计一个常用规则),当Native函数执行完成之后,如果局部引用没有被Native代码显式删除,那么局部引用在Java虚拟机中还是有效的。Java虚拟机来决定在什么时候来删除这个对象,而且直到JAVA层没有对它的引用(可以通过Native函数返回而把它引用到JAVA层),它才能被JVM回收并释放。这和C语言的局部变量概念是不同的。这也可以解释为什么Natvie函数能够以一个局部引用为返回值了。
局部引用在Native代码显示释放非常重要。你可能会问,既然Java虚拟机会自动释放局部变量为什么还需要我在Native代码中显示释放呢?原因有以下几点:
1、Java虚拟机默认为Native引用分配的局部引用数量是有限的,大部分的Java虚拟机实现默认分配16个局部引用。当然Java虚拟机也提供API(PushLocalFrame,EnsureLocalCapacity)让你申请更多的局部引用数量(Java虚拟机不保证你一定能申请到)。有限的资源当然要省着点用,否则将会被Java虚拟机无情抛弃(程序崩溃)。JNI编程中,实现Native代码时强烈建议调用PushLocalFrame,EnsureLocalCapacity来确保Java虚拟机为你准备好了局部变量空间。
2、如果你实现的Native函数是工具函数,会被频繁的调用。如果你在Native函数中没有显示删除局部引用,那么每次调用该函数Java虚拟机都会创建一个新的局部引用,造成局部引用过多。尤其是该函数在Native代码中被频繁调用,代码的控制权没有交还给Java虚拟机,所以Java虚拟机根本没有机会释放这些局部变量。退一步讲,就算该函数直接返回给Java虚拟机,也不能保证没有问题,我们不能假设Native函数返回Java虚拟机之后,Java虚拟机马上就会回收Native函数中创建的局部引用,依赖于Java虚拟机实现。所以我们在实现Native函数时一定要记着删除不必要的局部引用,否则你的程序就有潜在的风险,不知道什么时候就会爆发。
3、如果你Native函数根本就不返回。比如消息循环函数——死循环等待消息,处理消息。如果你不显示删除局部引用,很快将会造成Java虚拟机的局部引用内存溢出。
在JNI中显示释放局部引用的函数为DeleteLocalRef,大家可以查看手册来了解调用方法。
在JDK1.2中为了方便管理局部引用,引入了三个函数——EnsureLocalCapacity、PushLocalFrame、PopLocalFrame。这里介绍一下PushLocalFrame和PopLocalFrame函数。这两个函数是成对使用的,先调用PushLocalFrame,然后创建局部引用,并对其进行处理,最后调用PushLocalFrame释放局部引用,这时Java虚拟机也可以对其指向的对象进行垃圾回收。可以用C语言的栈来理解这对JNI API,调用PushLocalFrame之后Native代码创建的所有局部引用全部入栈,当调用PopLocalFrame之后,入栈的局部引用除了需要返回的局部引用(PushLocalFrame和PopLocalFrame这对函数可以返回一个局部引用给外部)之外,全部出栈,Java虚拟机这时可以释放他们指向的对象。具体的用法可以参考手册。这两个函数使JNI的局部引用由于和C语言的局部变量用法类似,所以强烈推荐使用。
当创建局部变量之后,Java虚拟机直到Native代码显示调用了DeleteLocalRef删除局部引用或从Native返回且没有另外的引用才能对该对象进行回收。Native代码调用DeleteLocalRef显示删除局部引用之后,Java虚拟机就可以对局部引用指向的对象垃圾回收了。当Native代码创建了局部引用,但未显示调用DeleteLocalRef删除局部引用,并返回Java虚拟机的话,那么由虚拟机来决定什么时候删除该局部引用,然后对其指向的对象垃圾回收。程序员不能对java虚拟机删除局部引用的时机进行假设。
局部引用仅仅对于java虚拟机当前调用上下文有效,不能够在多次调用上下文中共享局部引用。这句话也可以这样理解:局部引用只对当前线程有效,多个线程之间不能共享局部引用。局部引用不能用C语言的静态变量或者全局变量来保存,否则第二次调用的时候,将会产生崩溃。
三、全局引用
在所有引用中,我觉得全局引用是最好理解的一个了。为什么呢?主要和C语言的全局变量非常相近。
我已经提到局部引用大部分是通过JNI API返回而创建的,而全局引用必须要在Native代码中显示的调用JNI API NewGlobalRef来创建,创建之后将一直有效,直到显示的调用DeleteGlobalRef来删除这个全局引用。请注意NewGlobalRef的第二个参数,既可以用一个局部引用,也可以用全局引用生成一个全局引用,当然也可以用弱全局引用生成一个全局引用,但是这中情况有特殊的用途,后文会介绍。
全局引用和局部引用一样,可以防止其指向的对象被Java虚拟机垃圾回收。与局部引用只在当前线程有效不同的是全局引用可以在多线程之间共享(如果是多线程编程需要注意同步问题)。
四、弱全局引用
弱全局引用和全局引用一样,可以在多个线程之间共享,但是并不强制进行显式的销毁。虽然在我们确定不再需要弱全局引用的时候,建议进行显式的销毁(调用DeleteWeakGlobalRef)。但是即使我们不显式的销毁弱全局引用,JAVA虚拟机也能在它认为必要的时候自动回收并销毁弱全局引用。创建弱全局引用请使用NewWeakGlobalRef,显式销毁弱全局引用请使用DeleteWeakGlobalRef。
与全局引用和局部引用能够阻止Java虚拟机垃圾回收其指向的对象不同,弱全局引用指向的对象随时都可以被Java虚拟机垃圾回收,所以使用弱全局变量的时候,要时刻记着:它所指向的对象可能已经被垃圾回收了。JNI API提供了引用比较函数IsSameObject,用弱全局引用和NULL进行比较,如果返回JNI_TRUE,则说明弱全局引用指向的对象已经被释放。需要重新初始化弱全局引用。根据上面的介绍你可能会写出如下的代码:
static jobject weak_global_ref = NULL;
if((*env)-IsSameObject(env, weak_global_ref, NULL) == JNI_TRUE)
{
/* Init week global referrence again */
weak_global_ref = NewWeakGlobalRef(...);
}
/* Process weak_global_ref */
上面这段代码表面上没有什么错误,但是我们忘了一点儿,Java虚拟机的垃圾回收随时都可能发生。假设如下情形:
1、通过引用比较函数IsSameObject判断弱全局引用是否有效的时候,返回JNI_FALSE,证明其指向对象有效。
2、这时Java虚拟机进行了垃圾回收,回收了弱全局引用指向的对象。
3、这样如果我们后面访问弱全局引用指向的对象,将会引发程序崩溃,因为弱全局引用指向对象已经被Java虚拟机回收了。
根据JNI标准手册《Weak Global References》中的介绍,我们可以有这样一个使用弱全局引用的方案。在使用全局引用之前,我们先通过NewLocalRef函数创建一个局部引用,然后使用这个局部引用来访问该对象进行处理,当完成处理之后,删除局部引用。局部引用可以阻止Java虚拟机回收其指向的对象,这样可以保证在处理期间弱全局引用和局部引用指向的对象不会被Java虚拟机回收。假如弱全局引用指向对象已经被Java虚拟机回收,则NewLocalRef函数将会返回NULL,则创建局部引用失败,这个返回值有助于我们判断是否需要重新初始化弱全局引用。我们可以写出如下的代码:
static jobject weak_global_ref = NULL;
jobject local_ref;
/* We ensure create local_ref success */
while ( week_global_ref == NULL
|| (local_ref = NewLocalRef(env, weak_global_ref)) == NULL )
{
/* Init week global referrence again */
weak_global_ref = NewWeakGlobalRef(...);
}
/* Process local_ref */
.....
(*env)-DeleteLocalRef(env, local_ref);
注意在《Java Native Interface: Programmer’s Guide and Specification》的例子中,有很多不是按照如上的代码实现的,那些代码是有潜在风险的,请各位朋友注意。
弱全局引用是可以用来缓存jclass对象,但是用全局引用来缓存jclass对象将非常的危险。这里需要简单介绍一下Native的共享库的卸载。当Class Loader释放完所有的class后,然后Class Loader会卸载Native的共享库。如果我们用全局引用来缓存jclass对象的话,根据前面对全局引用对Java虚拟机垃圾回收机制的影响,将会阻止Java虚拟机回收该对象。如果我们不显式的释放全局引用(通过DeleteGlobalRef),则Class Loader也将不能释放这个jclass对象,进而造成Class Loader不能卸载Native的共享库(永远无法释放)。如果用弱全局引用来缓存将不会有这个问题,Java虚拟机随时都可以释放它指向的对象。
五、总结
至此我们把JNI规范中的三种引用都进行了一个简单的介绍,在此我对以上内容做一个简单总结:
1、局部引用是Native代码中最常用的引用。大部分局部引用都是通过JNI API返回来创建,也可以通过调用NewLocalRef来创建。另外强烈建议Native函数返回值为局部引用。局部引用只在当前调用上下文中有效,所以局部引用不能用Native代码中的静态变量和全局变量来保存。另外时刻要记着Java虚拟机局部引用的个数是有限的,编程的时候强烈建议调用EnsureLocalCapacity,PushLocalFrame和PopLocalFrame来确保Native代码能够获得足够的局部引用数量。
2、全局变量必须要通过NewGlobalRef创建,通过DeleteGlobalRef删除。主要用来缓存Field ID和Method ID。全局引用可以在多线程之间共享其指向的对象。在C语言中以静态变量和全局变量来保存。
3、全局引用和局部引用可以阻止Java虚拟机回收其指向的对象。
4、弱全局引用必须要通过NewWeakGlobalRef创建,通过DeleteWeakGlobalRef销毁。可以在多线程之间共享其指向的对象。在C语言中通过静态变量和全局变量来保持弱全局引用。弱全局引用指向的对象随时都可能会被Java虚拟机回收,所以使用的时候需要时刻注意检查其有效性。弱全局引用经常用来缓存jclass对象。
5、全局引用和弱全局引用可以在多线程中共享其指向对象,但是在多线程编程中需要注意多线程同步。强烈建议在JNI_OnLoad初始化全局引用和弱全局引用,然后在多线程中进行读全局引用和弱全局引用,这样不需要对全局引用和弱全局引用同步(只有读操作不会出现不一致情况)。