JAVA
中的内存泄漏
JAVA
编程中的内存泄漏,从泄漏的内存位置角度可以分为两种:JVM
中Java Heap
的内存泄漏;JVM
内存中native memory
的内存泄漏。
Java Heap
的内存泄漏
Java
对象存储在JVM
进程空间中的Java Heap
中,Java Heap
可以在JVM
运行过程中动态变化。如果Java
对象越来越多,占据Java Heap
的空间也越来越大,JVM
会在运行时扩充Java Heap
的容量。如果Java Heap
容量扩充到上限,并且在GC
后仍然没有足够空间分配新的Java
对象,便会抛出out of memory
异常,导致JVM
进程崩溃。
Java Heap
中out of memory
异常的出现有两种原因——①程序过于庞大,致使过多Java
对象的同时存在;②程序编写的错误导致Java Heap
内存泄漏。
多种原因可能导致Java Heap
内存泄漏。JNI
编程错误也可能导致Java Heap
的内存泄漏。
JVM
中native memory
的内存泄漏
从操作系统角度看,JVM
在运行时和其它进程没有本质区别。在系统级别上,它们具有同样的调度机制,同样的内存分配方式,同样的内存格局。
JVM
进程空间中,Java Heap
以外的内存空间称为JVM
的native memory
。进程的很多资源都是存储在JVM
的native memory
中,例如载入的代码映像,线程的堆栈,线程的管理控制块,JVM
的静态数据、全局数据等等。也包括JNI
程序中native code
分配到的资源。
在JVM
运行中,多数进程资源从native memory
中动态分配。当越来越多的资源在native memory
中分配,占据越来越多native memory
空间并且达到native memory
上限时,JVM
会抛出异常,使JVM
进程异常退出。而此时Java Heap
往往还没有达到上限。
多种原因可能导致JVM
的native memory
内存泄漏。例如JVM
在运行中过多的线程被创建,并且在同时运行。JVM
为线程分配的资源就可能耗尽native memory
的容量。
JNI
编程错误也可能导致native memory
的内存泄漏。对这个话题的讨论是本文的重点。
JNI
编程中明显的内存泄漏
JNI
编程实现了native code
和Java
程序的交互,因此JNI
代码编程既遵循 native code
编程语言的编程规则,同时也遵守JNI
编程的文档规范。在内存管理方面,native code
编程语言本身的内存管理机制依然要遵循,同时也要考虑JNI
编程的内存管理。
本章简单概括JNI
编程中显而易见的内存泄漏。从native code
编程语言自身的内存管理,和JNI
规范附加的内存管理两方面进行阐述。
Native Code
本身的内存泄漏
JNI
编程首先是一门具体的编程语言,或者C
语言,或者C++
,或者汇编,或者其它native
的编程语言。每门编程语言环境都实现了自身的内存管理机制。因此,JNI
程序开发者要遵循native
语言本身的内存管理机制,避免造成内存泄漏。以C
语言为例,当用malloc()
在进程堆中动态分配内存时,JNI
程序在使用完后,应当调用free()
将内存释放。总之,所有在native
语言编程中应当注意的内存泄漏规则,在JNI
编程中依然适应。
Native
语言本身引入的内存泄漏会造成native memory
的内存,严重情况下会造成native memory
的out of memory
。
Global Reference
引入的内存泄漏
JNI
编程还要同时遵循JNI
的规范标准,JVM
附加了JNI
编程特有的内存管理机制。
JNI
中的Local Reference
只在native method
执行时存在,当native method
执行完后自动失效。这种自动失效,使得对Local Reference
的使用相对简单,native method
执行完后,它们所引用的Java
对象的reference count
会相应减1
。不会造成Java Heap
中Java
对象的内存泄漏。
而Global Reference
对Java
对象的引用一直有效,因此它们引用的Java
对象会一直存在Java Heap
中。程序员在使用Global Reference
时,需要仔细维护对Global Reference
的使用。如果一定要使用Global Reference
,务必确保在不用的时候删除。就像在C
语言中,调用malloc()
动态分配一块内存之后,调用free()
释放一样。否则,Global Reference
引用的Java
对象将永远停留在Java Heap
中,造成Java Heap
的内存泄漏。
JNI
编程中潜在的内存泄漏——对LocalReference
的深入理解
Local Reference
在native method
执行完成后,会自动被释放,似乎不会造成任何的内存泄漏。但这是错误的。对Local Reference
的理解不够,会造成潜在的内存泄漏。
本章重点阐述Local Reference
使用不当可能引发的内存泄漏。引入两个错误实例,也是JNI
程序员容易忽视的错误;在此基础上介绍Local Reference
表,对比native method
中的局部变量和JNI Local Reference
的不同,使读者深入理解JNI Local Reference
的实质;最后为JNI
程序员提出应该如何正确合理使用JNI Local Reference
,以避免内存泄漏。
错误实例 1
在某些情况下,我们可能需要在native method
里面创建大量的JNI Local Reference
。这样可能导致 native memory
的内存泄漏,如果在native method
返回之前native memory
已经被用光,就会导致native memory
的out of memory
。
在代码清单 1 里,我们循环执行count
次,JNI functionNewStringUTF()
在每次循环中从Java Heap
中创建一个String
对象,str
是Java Heap
传给JNI native method
的Local Reference
,每次循环中新创建的String
对象覆盖上次循环中str
的内容。str
似乎一直在引用到一个String
对象。整个运行过程中,我们看似只创建一个Local Reference
。
执行代码清单1
的程序,第一部分为Java
代码,nativeMethod(int i)
中,输入参数设定循环的次数。第二部分为JNI
代码,用C
语言实现了nativeMethod(int i)
。
清单 1. Local Reference
引发内存泄漏
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 |
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(int i)的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
运行异常终止,原因是创建了过多的Local Reference
,从而导致out of memory
。实际上,nativeMethod
在运行中创建了越来越多的JNI Local Reference
,而不是看似的始终只有一个。过多的Local Reference
,导致了JNI
内部的JNI Local Reference
表内存溢出。
错误实例 2
实例 2 是实例 1 的变种,Java
代码未作修改,但是nativeMethod(int i)
的C
语言实现稍作修改。在JNI
的native method
中实现的utility
函数中创建Java
的String
对象。utility
函数只建立一个String
对象,返回给调用函数,但是utility
函数对调用者的使用情况是未知的,每个函数都可能调用它,并且同一函数可能调用它多次。在实例 2 中,nativeMethod
在循环中调用count
次,utility
函数在创建一个String
对象后即返回,并且会有一个退栈过程,似乎所创建的Local Reference
会在退栈时被删除掉,所以应该不会有很多Local Reference
被创建。实际运行结果并非如此。
清单 2. Local Reference
引发内存泄漏
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
Java 代码部分参考实例 1,未做任何修改。 JNI 代码,nativeMethod(int i) 的 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 的完全相同。过多的Local Reference
被创建,仍然导致了JNI
内部的 JNI Local Reference
表内存溢出。实际上,在utility
函数CreateStringUTF(JNIEnv * env)
执行完成后的退栈过程中,创建的Local Reference
并没有像native code
中的局部变量那样被删除,而是继续在Local Reference
表中存在,并且有效。Local Reference
和局部变量有着本质的区别。
Local Reference
深层解析
Java
JNI
的文档规范只描述了JNI Local Reference
是什么(存在的目的),以及应该怎么使用Local Reference
(开放的接口规范)。但是对Java
虚拟机中JNI Local Reference
的实现并没有约束,不同的Java
虚拟机有不同的实现机制。这样的好处是,不依赖于具体的JVM
实现,有好的可移植性;并且开发简单,规定了“应该怎么做、怎么用”。但是弊端是初级开发者往往看不到本质,“不知道为什么这样做”。对Local Reference
没有深层的理解,就会在编程过程中无意识的犯错。
Local Reference
和Local Reference
表
理解Local Reference
表的存在是理解JNI Local Reference
的关键。
JNI Local Reference
的生命期是在native method
的执行期(从Java
程序切换到native code
环境时开始创建,或者在native method
执行时调用JNI function
创建),在 native method
执行完毕切换回Java
程序时,所有JNI Local Reference
被删除,生命期结束(调用JNI function
可以提前结束其生命期)。
实际上,每当线程从Java
环境切换到native code
上下文时(J2N
),JVM
会分配一块内存,创建一个Local Reference
表,这个表用来存放本次native method
执行中创建的所有的Local Reference
。每当在native code
中引用到一个Java
对象时,JVM
就会在这个表中创建一个Local Reference
。比如,实例 1 中我们调用NewStringUTF()
在Java Heap
中创建一个String
对象后,在Local Reference
表中就会相应新增一个Local Reference
。
图 1. Local Reference
表、Local Reference
和Java
对象的关系
图 1 中:
⑴运行native method
的线程的堆栈记录着Local Reference
表的内存位置(指针 p)。
⑵ Local Reference
表中存放JNI Local Reference
,实现Local Reference
到Java
对象的映射。
⑶ native method
代码间接访问Java
对象(java obj1,java obj2)。通过指针p
定位相应的Local Reference
的位置,然后通过相应的Local Reference
映射到Java
对象。
⑷ 当native method
引用一个Java
对象时,会在Local Reference
表中创建一个新Local Reference
。在Local Reference
结构中写入内容,实现Local Reference
到Java
对象的映射。
⑸native method
调用DeleteLocalRef()
释放某个JNI Local Reference
时,首先通过指针p
定位相应的Local Reference
在Local Ref
表中的位置,然后从Local Ref
表中删除该Local Reference
,也就取消了对相应Java
对象的引用(Ref count
减1
)。
⑹当越来越多的Local Reference
被创建,这些Local Reference
会在Local Ref
表中占据越来越多内存。当Local Reference
太多以至于Local Ref
表的空间被用光,JVM
会抛出异常,从而导致JVM
的崩溃。
Local Ref
不是native code
的局部变量
很多人会误将JNI
中的Local Reference
理解为Native Code
的局部变量。这是错误的。
Native Code
的局部变量和Local Reference
是完全不同的,区别可以总结为:
⑴局部变量存储在线程堆栈中,而Local Reference
存储在Local Ref
表中。
⑵局部变量在函数退栈后被删除,而Local Reference
在调用DeleteLocalRef()
后才会从Local Ref
表中删除,并且失效,或者在整个Native Method
执行结束后被删除。
⑶ 可以在代码中直接访问局部变量,而Local Reference
的内容无法在代码中直接访问,必须通过JNI function
间接访问。JNI function
实现了对Local Reference
的间接访问,JNI function
的内部实现依赖于具体JVM
。
代码清单 1 中 str = (*env)->NewStringUTF(env, "0");
str
是jstring
类型的局部变量。Local Ref
表中会新创建一个Local Reference
,引用到NewStringUTF(env, "0")
在Java Heap
中新建的String
对象。如图 2 所示:
图 2. str 间接引用 string 对象
图 2 中,str 是局部变量,在 native method 堆栈中。Local Ref3 是新创建的 Local Reference,在 Local Ref 表中,引用新创建的 String 对象。JNI 通过 str 和指针 p 间接定位 Local Ref3,但 p 和 Local Ref3 对 JNI 程序员不可见。
Local Reference
导致内存泄漏
在以上论述基础上,我们通过分析错误实例 1 和实例 2,来分析 Local Reference 可能导致的内存泄漏,加深对 Local Reference 的深层理解。
分析错误实例 1:
局部变量str
在每次循环中都被重新赋值,间接指向最新创建的Local Reference
,前面创建的Local Reference 一直保留在 Local Ref 表中。
在实例 1 执行完第i
次循环后,内存布局如图 3:
图 3. 执行i
次循环后的内存布局
继续执行完第i+1
次循环后,内存布局发生变化,如图 4:
图 4. 执行i+1
次循环后的内存布局
图 4 中,局部变量 str 被赋新值,间接指向了Local Ref i+1
。在native method
运行过程中,我们已经无法释放Local Ref i
占用的内存,以及Local Ref i
所引用的第i
个 string
对象所占据的Java Heap
内存。所以,native memory
中Local Ref i
被泄漏,Java Heap
中创建的第 i
个string
对象被泄漏了。
也就是说在循环中,前面创建的所有i
个Local Reference
都泄漏了native memory
的内存,创建的所有 i 个string
对象都泄漏了Java Heap
的内存。
直到native memory
执行完毕,返回到Java
程序时(N2J
),这些泄漏的内存才会被释放,但是 Local Reference 表所分配到的内存往往很小,在很多情况下N2J
之前可能已经引发严重内存泄漏,导致Local Reference
表的内存耗尽,使JVM
崩溃,例如错误实例 1。
分析错误实例 2:
实例 2 与实例 1 相似,虽然每次循环中调用工具函数CreateStringUTF(env)
来创建对象,但是在CreateStringUTF(env)
返回退栈过程中,只是局部变量被删除,而每次调用创建的Local Reference
仍然存在Local Ref
表中,并且有效引用到每个新创建的string
对象。str
局部变量在每次循环中被赋新值。
这样的内存泄漏是潜在的,但是这样的错误 在JNI
程序员编程过程中却经常出现。通常情况,在触发out of memory
之前,native method
已经执行完毕,切换回 Java 环境,所有 Local Reference 被删除,问题也就没有显露出来。但是某些情况下就会引发out of memory
,导致实例 1 和实例 2 中的JVM
崩溃。
控制Local Reference
生命期
因此,在JNI
编程时,正确控制JNI
Local Reference
的生命期。如果需要创建过多的Local Reference
,那么在对被引用的Java
对象操作结束后,需要调用JNI function
(如 DeleteLocalRef()
),及时将JNI
Local Reference
从Local Ref
表中删除,以避免潜在的内存泄漏。
总结
本文阐述了JNI
编程可能引发的内存泄漏,JNI
编程既可能引发Java Heap
的内存泄漏,也可能引发native memory
的内存泄漏,严重的情况可能使JVM
运行异常终止。JNI
软件开发人员在编程中,应当考虑以下几点,避免内存泄漏:
native code
本身的内存管理机制依然要遵循。- 使用
Global reference
时,当native code
不再需要访问Global reference
时,应当调用JNI
函数DeleteGlobalRef()
删除Global reference
和它引用的Java
对象。Global reference
管理不当会导致Java Heap
的内存泄漏。 - 透彻理解
Local reference
,区分Local reference
和native code
的局部变量,避免混淆两者所引起的native memory
的内存泄漏。 - 使用
Local reference
时,如果Local reference
引用了大的Java
对象,当不再需要访问Local reference
时,应当调用JNI
函数DeleteLocalRef()
删除Local reference
,从而也断开对Java
对象的引用。这样可以避免Java Heap
的out of memory
。 - 使用
Local reference
时,如果在native method
执行期间会创建大量的Local reference
,当不再需要访问Local reference
时,应当调用JNI
函数DeleteLocalRef()
删除Local reference
。Local reference
表空间有限,这样可以避免Local reference
表的内存溢出,避免native memory
的out of memory
。 - 严格遵循
Java
JNI
规范书中的使用规则。