在 JNI 编程中避免内存泄漏

JNI编程简介

JNI,JavaNativeInterface,是nativecode的编程接口。JNI使Java代码程序可以与nativecode交互——在Java程序中调用nativecode;在nativecode中嵌入Java虚拟机调用Java的代码。

JNI编程在软件开发中运用广泛,其优势可以归结为以下几点:

利用nativecode的平台相关性,在平台相关的编程中彰显优势。

对nativecode的代码重用。

nativecode底层操作,更加高效。

然而任何事物都具有两面性,JNI编程也同样如此。程序员在使用JNI时应当认识到JNI编程中如下的几点弊端,扬长避短,才可以写出更加完善、高性能的代码:

从Java环境到nativecode的上下文切换耗时、低效。

JNI编程,如果操作不当,可能引起Java虚拟机的崩溃。

JNI编程,如果操作不当,可能引起内存泄漏。

回页首

JAVA中的内存泄漏

JAVA编程中的内存泄漏,从泄漏的内存位置角度可以分为两种:JVM中JavaHeap的内存泄漏;JVM内存中nativememory的内存泄漏。

JavaHeap的内存泄漏

Java对象存储在JVM进程空间中的JavaHeap中,JavaHeap可以在JVM运行过程中动态变化。如果Java对象越来越多,占据JavaHeap的空间也越来越大,JVM会在运行时扩充JavaHeap的容量。如果JavaHeap容量扩充到上限,并且在GC后仍然没有足够空间分配新的Java对象,便会抛出outofmemory异常,导致JVM进程崩溃。

JavaHeap中outofmemory异常的出现有两种原因——①程序过于庞大,致使过多Java对象的同时存在;②程序编写的错误导致JavaHeap内存泄漏。

多种原因可能导致JavaHeap内存泄漏。JNI编程错误也可能导致JavaHeap的内存泄漏。

JVM中nativememory的内存泄漏

从操作系统角度看,JVM在运行时和其它进程没有本质区别。在系统级别上,它们具有同样的调度机制,同样的内存分配方式,同样的内存格局。

JVM进程空间中,JavaHeap以外的内存空间称为JVM的nativememory。进程的很多资源都是存储在JVM的nativememory中,例如载入的代码映像,线程的堆栈,线程的管理控制块,JVM的静态数据、全局数据等等。也包括JNI程序中nativecode分配到的资源。

在JVM运行中,多数进程资源从nativememory中动态分配。当越来越多的资源在nativememory中分配,占据越来越多nativememory空间并且达到nativememory上限时,JVM会抛出异常,使JVM进程异常退出。而此时JavaHeap往往还没有达到上限。

多种原因可能导致JVM的nativememory内存泄漏。例如JVM在运行中过多的线程被创建,并且在同时运行。JVM为线程分配的资源就可能耗尽nativememory的容量。

JNI编程错误也可能导致nativememory的内存泄漏。对这个话题的讨论是本文的重点。

回页首

JNI编程中明显的内存泄漏

JNI编程实现了nativecode和Java程序的交互,因此JNI代码编程既遵循nativecode编程语言的编程规则,同时也遵守JNI编程的文档规范。在内存管理方面,nativecode编程语言本身的内存管理机制依然要遵循,同时也要考虑JNI编程的内存管理。

本章简单概括JNI编程中显而易见的内存泄漏。从nativecode编程语言自身的内存管理,和JNI规范附加的内存管理两方面进行阐述。

NativeCode本身的内存泄漏

JNI编程首先是一门具体的编程语言,或者C语言,或者C++,或者汇编,或者其它native的编程语言。每门编程语言环境都实现了自身的内存管理机制。因此,JNI程序开发者要遵循native语言本身的内存管理机制,避免造成内存泄漏。以C语言为例,当用malloc()在进程堆中动态分配内存时,JNI程序在使用完后,应当调用free()将内存释放。总之,所有在native语言编程中应当注意的内存泄漏规则,在JNI编程中依然适应。

Native语言本身引入的内存泄漏会造成nativememory的内存,严重情况下会造成nativememory的outofmemory。

GlobalReference引入的内存泄漏

JNI编程还要同时遵循JNI的规范标准,JVM附加了JNI编程特有的内存管理机制。

JNI中的LocalReference只在nativemethod执行时存在,当nativemethod执行完后自动失效。这种自动失效,使得对LocalReference的使用相对简单,nativemethod执行完后,它们所引用的Java对象的referencecount会相应减1。不会造成JavaHeap中Java对象的内存泄漏。

而GlobalReference对Java对象的引用一直有效,因此它们引用的Java对象会一直存在JavaHeap中。程序员在使用GlobalReference时,需要仔细维护对GlobalReference的使用。如果一定要使用GlobalReference,务必确保在不用的时候删除。就像在C语言中,调用malloc()动态分配一块内存之后,调用free()释放一样。否则,GlobalReference引用的Java对象将永远停留在JavaHeap中,造成JavaHeap的内存泄漏。

回页首

JNI编程中潜在的内存泄漏——对LocalReference的深入理解

LocalReference在nativemethod执行完成后,会自动被释放,似乎不会造成任何的内存泄漏。但这是错误的。对LocalReference的理解不够,会造成潜在的内存泄漏。

本章重点阐述LocalReference使用不当可能引发的内存泄漏。引入两个错误实例,也是JNI程序员容易忽视的错误;在此基础上介绍LocalReference表,对比nativemethod中的局部变量和JNILocalReference的不同,使读者深入理解JNILocalReference的实质;最后为JNI程序员提出应该如何正确合理使用JNILocalReference,以避免内存泄漏。

错误实例1

在某些情况下,我们可能需要在nativemethod里面创建大量的JNILocalReference。这样可能导致nativememory的内存泄漏,如果在nativemethod返回之前nativememory已经被用光,就会导致nativememory的outofmemory。

在代码清单1里,我们循环执行count次,JNIfunctionNewStringUTF()在每次循环中从JavaHeap中创建一个String对象,str是JavaHeap传给JNInativemethod的LocalReference,每次循环中新创建的String对象覆盖上次循环中str的内容。str似乎一直在引用到一个String对象。整个运行过程中,我们看似只创建一个LocalReference。

执行代码清单1的程序,第一部分为Java代码,nativeMethod(inti)中,输入参数设定循环的次数。第二部分为JNI代码,用C语言实现了nativeMethod(inti)。

清单1.LocalReference引发内存泄漏

Java代码部分

class TestLocalReference { 
 private native void nativeMethod(int i); 
 public static void main(String args[]) { 
         TestLocalReference c = new TestLocalReference(); 
         //call the jni native method 
         c.nativeMethod(1000000); 
 }  
 static { 
 //load the jni library 
 System.loadLibrary("StaticMethodCall"); 
 } 
 }

JNI代码,nativeMethod(inti)的C语言实现

#include<stdio.h> 
 #include<jni.h> 
 #include"TestLocalReference.h"
 JNIEXPORT void JNICALL Java_TestLocalReference_nativeMethod 
 (JNIEnv * env, jobject obj, jint count) 
 { 
 jint i = 0; 
 jstring str; 

 for(; i<count; i++) 
         str = (*env)->NewStringUTF(env, "0"); 
 }

运行结果

JVMCI161: FATAL ERROR in native method: Out of memory when expanding 
 local ref table beyond capacity 
 at TestLocalReference.nativeMethod(Native Method) 
 at TestLocalReference.main(TestLocalReference.java:9)

运行结果证明,JVM运行异常终止,原因是创建了过多的LocalReference,从而导致outofmemory。实际上,nativeMethod在运行中创建了越来越多的JNILocalReference,而不是看似的始终只有一个。过多的LocalReference,导致了JNI内部的JNILocalReference表内存溢出。

错误实例2

实例2是实例1的变种,Java代码未作修改,但是nativeMethod(inti)的C语言实现稍作修改。在JNI的nativemethod中实现的utility函数中创建Java的String对象。utility函数只建立一个String对象,返回给调用函数,但是utility函数对调用者的使用情况是未知的,每个函数都可能调用它,并且同一函数可能调用它多次。在实例2中,nativeMethod在循环中调用count次,utility函数在创建一个String对象后即返回,并且会有一个退栈过程,似乎所创建的LocalReference会在退栈时被删除掉,所以应该不会有很多LocalReference被创建。实际运行结果并非如此。

清单2.LocalReference引发内存泄漏

Java代码部分参考实例1,未做任何修改。

JNI代码,nativeMethod(inti)的C语言实现

#include<stdio.h> 
 #include<jni.h> 
 #include"TestLocalReference.h"
 jstring CreateStringUTF(JNIEnv * env) 
 { 
 return (*env)->NewStringUTF(env, "0"); 
 } 
 JNIEXPORT void JNICALL Java_TestLocalReference_nativeMethod 
 (JNIEnv * env, jobject obj, jint count) 
 { 
 jint i = 0; 
 for(; i<count; i++) 
 { 
         str = CreateStringUTF(env); 
 } 
 }

运行结果

JVMCI161: FATAL ERROR in native method: Out of memory when expanding local ref 
 table beyond  capacity 
 at TestLocalReference.nativeMethod(Native Method) 
 at TestLocalReference.main(TestLocalReference.java:9)

运行结果证明,实例2的结果与实例1的完全相同。过多的LocalReference被创建,仍然导致了JNI内部的JNILocalReference表内存溢出。实际上,在utility函数CreateStringUTF(JNIEnv*env)

执行完成后的退栈过程中,创建的LocalReference并没有像nativecode中的局部变量那样被删除,而是继续在LocalReference表中存在,并且有效。LocalReference和局部变量有着本质的区别。

LocalReference深层解析

JavaJNI的文档规范只描述了JNILocalReference是什么(存在的目的),以及应该怎么使用LocalReference(开放的接口规范)。但是对Java虚拟机中JNILocalReference的实现并没有约束,不同的Java虚拟机有不同的实现机制。这样的好处是,不依赖于具体的JVM实现,有好的可移植性;并且开发简单,规定了“应该怎么做、怎么用”。但是弊端是初级开发者往往看不到本质,“不知道为什么这样做”。对LocalReference没有深层的理解,就会在编程过程中无意识的犯错。

LocalReference和LocalReference表

理解LocalReference表的存在是理解JNILocalReference的关键。

JNILocalReference的生命期是在nativemethod的执行期(从Java程序切换到nativecode环境时开始创建,或者在nativemethod执行时调用JNIfunction创建),在nativemethod执行完毕切换回Java程序时,所有JNILocalReference被删除,生命期结束(调用JNIfunction可以提前结束其生命期)。

实际上,每当线程从Java环境切换到nativecode上下文时(J2N),JVM会分配一块内存,创建一个LocalReference表,这个表用来存放本次nativemethod执行中创建的所有的LocalReference。每当在nativecode中引用到一个Java对象时,JVM就会在这个表中创建一个LocalReference。比如,实例1中我们调用NewStringUTF()在JavaHeap中创建一个String对象后,在LocalReference表中就会相应新增一个LocalReference。

图1.LocalReference表、LocalReference和Java对象的关系

图1中:

⑴运行nativemethod的线程的堆栈记录着LocalReference表的内存位置(指针p)。

⑵LocalReference表中存放JNILocalReference,实现LocalReference到Java对象的映射。

⑶nativemethod代码间接访问Java对象(javaobj1,javaobj2)。通过指针p定位相应的LocalReference的位置,然后通过相应的LocalReference映射到Java对象。

⑷当nativemethod引用一个Java对象时,会在LocalReference表中创建一个新LocalReference。在LocalReference结构中写入内容,实现LocalReference到Java对象的映射。

⑸nativemethod调用DeleteLocalRef()释放某个JNILocalReference时,首先通过指针p定位相应的LocalReference在LocalRef表中的位置,然后从LocalRef表中删除该LocalReference,也就取消了对相应Java对象的引用(Refcount减1)。

⑹当越来越多的LocalReference被创建,这些LocalReference会在LocalRef表中占据越来越多内存。当LocalReference太多以至于LocalRef表的空间被用光,JVM会抛出异常,从而导致JVM的崩溃。

LocalRef不是nativecode的局部变量

很多人会误将JNI中的LocalReference理解为NativeCode的局部变量。这是错误的。

NativeCode的局部变量和LocalReference是完全不同的,区别可以总结为:

⑴局部变量存储在线程堆栈中,而LocalReference存储在LocalRef表中。

⑵局部变量在函数退栈后被删除,而LocalReference在调用DeleteLocalRef()后才会从LocalRef表中删除,并且失效,或者在整个NativeMethod执行结束后被删除。

⑶可以在代码中直接访问局部变量,而LocalReference的内容无法在代码中直接访问,必须通过JNIfunction间接访问。JNIfunction实现了对LocalReference的间接访问,JNIfunction的内部实现依赖于具体JVM。

代码清单1中str=(*env)->NewStringUTF(env,"0");

str是jstring类型的局部变量。LocalRef表中会新创建一个LocalReference,引用到NewStringUTF(env,"0")在JavaHeap中新建的String对象。如图2所示:

图2.str间接引用string对象

图2中,str是局部变量,在nativemethod堆栈中。LocalRef3是新创建的LocalReference,在LocalRef表中,引用新创建的String对象。JNI通过str和指针p间接定位LocalRef3,但p和LocalRef3对JNI程序员不可见。

LocalReference导致内存泄漏

在以上论述基础上,我们通过分析错误实例1和实例2,来分析LocalReference可能导致的内存泄漏,加深对LocalReference的深层理解。

分析错误实例1:

局部变量str在每次循环中都被重新赋值,间接指向最新创建的LocalReference,前面创建的LocalReference一直保留在LocalRef表中。

在实例1执行完第i次循环后,内存布局如图3:

图3.执行i次循环后的内存布局

继续执行完第i+1次循环后,内存布局发生变化,如图4:

图4.执行i+1次循环后的内存布局

图4中,局部变量str被赋新值,间接指向了LocalRefi+1。在nativemethod运行过程中,我们已经无法释放LocalRefi占用的内存,以及LocalRefi所引用的第i个string对象所占据的JavaHeap内存。所以,nativememory中LocalRefi被泄漏,JavaHeap中创建的第i个string对象被泄漏了。

也就是说在循环中,前面创建的所有i个LocalReference都泄漏了nativememory的内存,创建的所有i个string对象都泄漏了JavaHeap的内存。

直到nativememory执行完毕,返回到Java程序时(N2J),这些泄漏的内存才会被释放,但是LocalReference表所分配到的内存往往很小,在很多情况下N2J之前可能已经引发严重内存泄漏,导致LocalReference表的内存耗尽,使JVM崩溃,例如错误实例1。

分析错误实例2:

实例2与实例1相似,虽然每次循环中调用工具函数CreateStringUTF(env)来创建对象,但是在CreateStringUTF(env)返回退栈过程中,只是局部变量被删除,而每次调用创建的LocalReference仍然存在LocalRef表中,并且有效引用到每个新创建的string对象。str局部变量在每次循环中被赋新值。

这样的内存泄漏是潜在的,但是这样的错误在JNI程序员编程过程中却经常出现。通常情况,在触发outofmemory之前,nativemethod已经执行完毕,切换回Java环境,所有LocalReference被删除,问题也就没有显露出来。但是某些情况下就会引发outofmemory,导致实例1和实例2中的JVM崩溃。

控制LocalReference生命期

因此,在JNI编程时,正确控制JNILocalReference的生命期。如果需要创建过多的LocalReference,那么在对被引用的Java对象操作结束后,需要调用JNIfunction(如DeleteLocalRef()),及时将JNILocalReference从LocalRef表中删除,以避免潜在的内存泄漏。

回页首

总结

本文阐述了JNI编程可能引发的内存泄漏,JNI编程既可能引发JavaHeap的内存泄漏,也可能引发nativememory的内存泄漏,严重的情况可能使JVM运行异常终止。JNI软件开发人员在编程中,应当考虑以下几点,避免内存泄漏:

nativecode本身的内存管理机制依然要遵循。

使用Globalreference时,当nativecode不再需要访问Globalreference时,应当调用JNI函数DeleteGlobalRef()删除Globalreference和它引用的Java对象。Globalreference管理不当会导致JavaHeap的内存泄漏。

透彻理解Localreference,区分Localreference和nativecode的局部变量,避免混淆两者所引起的nativememory的内存泄漏。

使用Localreference时,如果Localreference引用了大的Java对象,当不再需要访问Localreference时,应当调用JNI函数DeleteLocalRef()删除Localreference,从而也断开对Java对象的引用。这样可以避免JavaHeap的outofmemory。

使用Localreference时,如果在nativemethod执行期间会创建大量的Localreference,当不再需要访问Localreference时,应当调用JNI函数DeleteLocalRef()删除Localreference。Localreference表空间有限,这样可以避免Localreference表的内存溢出,避免nativememory的outofmemory。

严格遵循JavaJNI规范书中的使用规则。

参考资料

学习

查看developerWorks文章“JavaprogrammingwithJNI”,了解JNI开发教程。

查看文章“Java(TM)NativeInterface:Programmer'sGuideandSpecification”,详细了解JNI编程向导和规范。

查看文章“WindowsJavaaddressspace”,了解JVM进程的内存空间布局。

developerWorksJava技术专区:数百篇关于Java编程各个方面的文章。

讨论

加入developerWorks中文社区。查看开发人员推动的博客、论坛、组和维基,并与其他developerWorks用户交流。

关于作者

解维东是IBM中国系统与技术实验室的软件工程师,担任IBMSystemsDirector的产品工程师,主要职责是解决客户报告的问题。在加入IBM之前,在因特尔公司做了10个月的Linux实习开发人员。2007年,毕业于中国南京大学,取得了硕士学位。

注:本人转自网络http://www.ibm.com/developerworks/cn/java/j-lo-jnileak/index.html?ca=drs-

相关推荐