JNI 相关内存泄漏[摘抄]

前几天的文章 解读《Android Camera内存问题剖析》 曾提到 Android 的 Java 代码调 C++ 引起的 native 内存泄漏。这里梳理一下 JNI 相关内存泄漏。

JNI

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

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

  1. 利用 native code 的平台相关性,在平台相关的编程中彰显优势。
  2. 对 native code 的代码重用。
  3. native code 底层操作,更加高效。

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

  1. 从 Java 环境到 native code 的上下文切换耗时、低效。
  2. JNI 编程,如果操作不当,可能引起 Java 虚拟机的崩溃。
  3. JNI 编程,如果操作不当,可能引起内存泄漏。

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 异常的出现有两种原因——

  1. 程序过于庞大,致使过多 Java 对象的同时存在;
  2. 程序编写的错误导致 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 编程的内存管理。

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 的内存泄漏。

LocalReference 造成的内存泄漏

Local Reference 在 native method 执行完成后,会自动被释放,似乎不会造成任何的内存泄漏。但这是错误的。对 Local Reference 的理解不够,会造成潜在的内存泄漏。示例不再赘述。

总结

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 规范书中的使用规则。

附 JNI 与 Java 类型对照表