在上篇文章中介绍了JNI常见错误,这篇文章将描述JNI开发中内存泄露问题。在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对象,便会抛出OutOfMemory错误,导致程序崩溃。
二、Native memory内存泄漏
从操作系统角度来看,JVM在运行时和其他进程没有本质的区别。在系统级别上,它们具有同样的调度机制,同样的内存分配方式,同样的内存格局。
多种原因可能导致JVM的native memory内存泄漏。例如:
- JVM在运行中过多的线程被创建,并且同时运行;
- JVM为线程分配的资源就可能耗尽native memory的容量;
- JNI编程错误也可能导致native memory的内存泄漏;
JNI编程错误造成的内存泄漏有以下几类:
1.Native Code本身造成的内存泄漏
在JNI编程中,会使用到native编程语言,例如C/C++。native编程语言都自己的内存管理机制,如果没有遵循native编程语言的内存管理机制,也会造成内存泄漏。例如,在C语言中,是通过malloc()函数分配内存,对应的释放内存函数是free()。如果分配内存后,没有调用相应的free()函数,则会造成native memory内存泄漏。
2.Global Reference(全局引用)引入的内存泄漏
JNI编程需要遵循JNI的规范标准,JVM附加了JNI编程特有的内存管理机制。在JNI中的Local Reference(局部引用)只在本地方法内有效,当本地方法返回时,会被自动失效。这种自动失效,会把它们所引用的Java对象中的引用计数减1,从而可以让Java中的对象被回收,不会造成Java Heap中Java对象的内存泄漏。
而JNI中的Global Reference(全局引用)对Java对象的引用一直有效,除非显式的调用DeleteGlobalRef方法释放全Global Reference。在使用Global Reference时,需要仔细维护对Global Reference的使用,在不使用的时候要及时释放Global Reference,否则Global Reference引用的Java对象将永远停留在Java Heap中,造成Java Heap的内存泄漏。
3.Local Reference可能造成的内存泄漏
虽然在本地方法返回时,JNI可以确保Local Reference引用的对象可以被自动回收。但还是存在以下几种情况,需要手动释放Local Reference,否则也会造成内存泄漏。
- 在本地方法中引用了很大的Java对象,在使用完该Java对象后,还需要执行一些耗时操作。如果不主动释放该Local Reference的话,则将一直保持对Java对象的引用,需要等到本地方法返回时,才能释放对Java对象的引用。在执行耗时操作期间,会由于Local Reference没有及时释放,导致Java Heap的内存泄漏。
- 在本地代码中创建了大量的Local Reference对象,没有及时手动释放,导致内存溢出。此时需要手动释放那些不再使用的Local Reference对象,例如在本地代码中创建一个很大的对象数组。
- 不返回的本地函数。例如:在一个本地函数中循环处理消息,如果不释放循环中使用的局部引用,则会无限地累积,进而导致内存泄漏。此时在循环体中需要手动释放局部引用。