(转)JNI内存管理及优化
最近接触JNI相关的项目遇到一些与内存相关到问题,JNI或者说是C/C++开发与平时的Java开发相比,总会给人更多的“惊喜”,因为不熟悉native开发,所以有很多知识都不太清楚,一点一点地记录。
在掘金相关搜索到下面这篇,个人感觉还不错。
以下内容转载自:
https://juejin.cn/post/6844903743352209422
上面这张图大家都应该很熟了,下面只讲下和JNI有关的部分
程序计数器
记录正在执行的虚拟机字节码指令的地址(如果正在执行的是本地方法则为空)。
本地方法栈
本地方法栈与 Java 虚拟机栈类似,它们之间的区别只不过是本地方法栈为本地方法服务。 本地方法一般是用其它语言(C、C++ 或汇编语言等)编写的,并且被编译为基于本机硬件和操作系统的程序,对待这些方法需要特别处理。
堆(Java-Heap)
所有对象都在这里分配内存,是垃圾收集的主要区域(”GC 堆”)。 堆不需要连续内存,并且可以动态增加其内存,增加失败会抛出 OutOfMemoryError 异常。
可以通过 -Xms 和 -Xmx 两个虚拟机参数来指定一个程序的堆内存大小,第一个参数设置初始值,第二个参数设置最大值。
1 | java -Xmx1024m -Xms1024m |
在Android系统对于每个应用都有内存使用的限制,机器的内存限制,在/system/build.prop文件中配置的。可以在manifest文件application节点加入 android:largeHeap="true"
来让Dalvik/ART虚拟机分配更大的堆内存空间
直接内存(native堆)
也称为C-Heap,供Java Runtime进程使用的,没有相应的参数来控制其大小,其大小依赖于操作系统进程的最大值。 Java应用程序都是在Java Runtime Environment(JRE)中运行,而Runtime本身就是由Native语言(如:C/C++)编写程序。Native Memory就是操作系统分配给Runtime进程的可用内存,它与Heap Memory不同,Java Heap 是Java应用程序的内存。。Native Memory的主要作用如下:
- 管理java heap的状态数据(用于GC);
- JNI调用,也就是Native Stack;
- JIT(即使编译器)编译时使用Native Memory,并且JIT的输入(Java字节码)和输出(可执行代码)也都是保存在Native Memory;
- NIO direct buffer;
- Threads;
- 类加载器和类信息都是保存在Native Memory中的。
JNI内存
在Java代码中,Java对象被存放在JVM的Java Heap,由垃圾回收器(Garbage Collector,即GC)自动回收就可以。
在Native代码中,内存是从Native Memory中分配的,需要根据Native编程规范去操作内存。如:C/C++使用malloc()/new分配内存,需要手动使用free()/delete回收内存。
然而,JNI和上面两者又有些区别。 JNI提供了与Java相对应的引用类型(如:jobject、jstring、jclass、jarray、jintArray等),以便Native代码可以通过JNI函数访问到Java对象。引用所指向的Java对象通常就是存放在Java Heap,而Native代码持有的引用是存放在Native Memory中。
举个例子,如下代码:
1 | jstring jstr = env->NewStringUTF("Hello World!"); |
- jstring类型是JNI提供的,对应于Java的String类型
- JNI函数
NewStringUTF()
用于构造一个String对象,该对象存放在Java Heap中,同时返回了一个jstring类型的引用。 - String对象的引用保存在jstr中,jstr是Native的一个局部变量,存放在Native Memory中。
开发人员都应该遇到过OOM(Out of Memory)异常,在JNI开发中,该异常可能发生在Java Heap中,也可能发生在Native Memory中。
- string类型是JNI提供的,对应于Java的String类型
- JNI函数
NewStringUTF()
用于构造一个String对象,该对象存放在Java Heap中,同时返回了一个jstring类型的引用。 - String对象的引用保存在jstr中,jstr是Native的一个局部变量,存放在Native Memory中。
开发人员都应该遇到过OOM(Out of Memory)异常,在JNI开发中,该异常可能发生在Java Heap中,也可能发生在Native Memory中。
1 | java.lang.OutOfMemoryError: Java heap space |
Java Heap 中出现 Out of Memory异常的原因有两种:
1 | 1)程序过于庞大,致使过多 Java 对象的同时存在; |
Native Memory中出现 Out of Memory异常的原因:
1 | 1)程序申请过多资源,系统未能满足,比如说大量线程资源; |
JNI引用
JNI引用有三种:Local Reference
、Global Reference
、Weak Global Reference
。下面分别来介绍一下这三种引用内存分配和管理。
Local Reference
只在Native Method执行时存在,只在创建它的线程有效,不能跨线程使用。它的生命期是在Native Method的执行期开始创建(从Java代码切换到Native代码环境时,或者在Native Method执行时调用JNI函数时),在Native Method执行完毕切换回Java代码时,所有Local Reference被删除(GC会回收其内存),生命期结束(调用DeleteLocalRef()
可以提前回收内存,结束其生命期)。
实际上,每当线程从Java环境切换到Native代码环境时,JVM 会分配一块内存用于创建一个Local Reference Table
,这个Table用来存放本次Native Method 执行中创建的所有Local Reference
。每当在 Native代码中引用到一个Java对象时,JVM 就会在这个Table中创建一个Local Reference
。比如,我们调用 NewStringUTF() 在 Java Heap 中创建一个 String 对象后,在 Local Reference Table
中就会相应新增一个 Local Reference
。
Local Reference 表、Local Reference 和 Java 对象的关系
接下来举个简单例子说明一下:
1 | jstring jstr = env->NewStringUTF("Hello World!"); |
- jstr存放在Native Method Stack中,是一个局部变量
- 对于开发者来说,Local Reference Table是不可见的
Local Reference Table
的内存不大,所能存放的Local Reference
数量也是有限的(在Android中默认最大容量是512个),使用不当就会引起溢出异常Local Reference
并不是Native里面的局部变量,局部变量存放在堆栈中,其引用存放在Local Reference Table
中。
在Native Method结束时,JVM会自动释放Local Reference,但Local Reference Table
是有大小限制的,在开发中应该及时使用DeleteLocalRef()删除不必要的Local Reference,不然可能会出现溢出错误:
1 | JNI ERROR (app bug): local reference table overflow (max=512) |
在C/C++中实例化的JNI对象,如果不返回java,必须用release掉或delete,否则内存泄露。包括NewStringUTF,NewObject。对于一般的基本数据类型(如:jint,jdouble等),是没必要调用该函数删除掉的。如果返回java不必delete,java会自己回收。
Global Reference
Local Reference是在Native Method执行的时候出现的,而Global Reference
是通过JNI函数NewGlobalRef()
和DeleteGlobalRef()
来创建和删除的。 Global Reference
具有全局性,可以在多个Native Method调用过程和多线程中使用,在主动调用DeleteGlobalRef之前,它是一直有效的(GC不会回收其内存)。
1 | /** |
使用 Global reference
时,当 native code 不再需要访问Global reference
时,应当调用 JNI 函数 DeleteGlobalRef()
删除 Global reference
和它引用的 Java 对象。否则Global Reference
引用的 Java 对象将永远停留在 Java Heap 中,从而导致 Java Heap 的内存泄漏。
Weak Global Reference
用NewWeakGlobalRef()
和DeleteWeakGlobalRef()
进行创建和删除,它与Global Reference
的区别在于该类型的引用随时都可能被GC回收。
对于Weak Global Reference
而言,可以通过isSameObject()
将其与NULL比较,看看是否已经被回收了。如果返回JNI_TRUE,则表示已经被回收了,需要重新初始化弱全局引用。Weak Global Reference
的回收时机是不确定的,有可能在前一行代码判断它是可用的,后一行代码就被GC回收掉了。为了避免这事事情发生,JNI官方给出了正确的做法,通过NewLocalRef()获取Weak Global Reference
,避免被GC回收。
注意点
Local Reference 不是 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。
注意释放所有对jobject的引用:
1 | extern "C" |
其它的还有:
1 | jclass ref= (env)->FindClass("java/lang/String"); |
因为根据jni.h
里的定义:
1 | typedef jobject jclass; |
jclass也是jobject。而jmethodID
/jfielID
和jobject没有继承关系,它们不是object,只是个整数,不存在被释放与否的问题。
局部引用和全局引用的转换
注意Local Reference的生命周期,如果在Native中需要长时间持有一个Java对象,就不能使用将jobject存储在Native,否则在下次使用的时候,即使同一个线程调用,也将会无法使用。下面是错误的做法:
1 | jstring global; |
正确的做法是使用Global Reference,如下:
1 | jstring global; |
多线程
JNIEnv和jobject对象都不能跨线程使用。 对于jobject,解决办法是
1 | a、m_obj = env->NewGlobalRef(obj);//创建一个全局变量 |
对于JNIEnv,解决办法是在每个线程中都重新生成一个env
1 | JavaVM *gJavaVM;//声明全局变量 |
当在一个线程里面调用AttachCurrentThr