JNI内存方面说明以及相关类型手动释放内存
6.1 Local Reference 不是 native code 的局部变量
一、Java内存
Java程序所涉及的内存可以从逻辑上划分为两部分:Heap Memory和Native Memory。
1)Heap Memory:
供Java应用程序使用的,所有java对象的内存都是从这里分配的,它不是物理上连续的,但是逻辑上是连续的。可通过java命令行参数“-Xms, -Xmx”大设置Heap初始值和最大值。
java -Xmx1024m -Xms1024m
//-Xmx1024m:设置JVM最大可用内存为1024M。
//-Xms1024m:设置JVM初始内存为1024m。此值可与-Xmx相同,以避免每次垃圾回收完成后JVM重新分配内存。
在Android系统对于每个应用都有内存使用的限制,机器的内存限制,在/system/build.prop文件中配置的。可以在manifest文件application节点加入 android:largeHeap="true"
来让Dalvik/ART虚拟机分配更大的堆内存空间
2)Native Memory:
也称为C-Heap,供Java Runtime进程使用的,没有相应的参数来控制其大小,其大小依赖于操作系统进程的最大值。
Java应用程序都是在Java Runtime Environment(JRE)中运行,而Runtime本身就是由Native语言(如:C/C++)编写程序。Native Memory就是操作系统分配给Runtime进程的可用内存,它与Heap Memory不同,Java Heap 是Java应用程序的内存。(JVM只是JRE的一部分,JVM的内存模型属于另一话题)
Native Memory的主要作用如下:
- 管理java heap的状态数据(用于GC);
- JNI调用,也就是Native Stack;
- JIT(即使编译器)编译时使用Native Memory,并且JIT的输入(Java字节码)和输出(可执行代码)也都是保存在Native Memory;
- NIO direct buffer;
- Threads;
- 类加载器和类信息都是保存在Native Memory中的。
由上可以得知,JNI内存分配其实与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中。
举个例子,如下代码:
jstring jstr = env->NewStringUTF("Hello World!");
1)jstring类型是JNI提供的,对应于Java的String类型。
2)JNI函数NewStringUTF()用于构造一个String对象,该对象存放在Java Heap中,同时返回了一个jstring类型的引用。
3)String对象的引用保存在jstr中,jstr是Native的一个局部变量,存放在Native Memory中。
开发人员都应该遇到过OOM(Out of Memory)异常,在JNI开发中,该异常可能发生在Java Heap中,也可能发生在Native Memory中。
java.lang.OutOfMemoryError: Java heap space
java.lang.OutOfMemoryError: native memory exhausted
Java Heap 中出现 Out of Memory异常的原因有两种:
1)程序过于庞大,致使过多 Java 对象的同时存在;
2)程序编写的错误导致 Java Heap 内存泄漏。
Native Memory中出现 Out of Memory异常的原因:
1)程序申请过多资源,系统未能满足,比如说大量线程资源;
2)程序编写的错误导致Native Memory内存泄漏。
为了避免出现OOM异常和内存泄露,我们在进行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 对象的关系
接下来举个简单例子说明一下:
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,不然可能会出现溢出错误:
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不会回收其内存)。
/**
* 创建obj参数所引用对象的新全局引用。obj参数既可以是全局引用,也可以是局部引用。全局引用通过调用DeleteGlobalRef()来显式撤消。
* @param obj 全局或局部引用。
* @return 返回全局引用。如果系统内存不足则返回 NULL。
*/
jobject NewGlobalRef(jobject obj);
/**
* 删除globalRef所指向的全局引用
* @param globalRef 全局引用
*/
void DeleteGlobalRef(jobject globalRef);
使用 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回收。
六、注意点
6.1 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。
6.2 注意释放所有对jobject的引用:
extern "C"
JNIEXPORT jstring JNICALL
Java_com_test_application_MainActivity_init(JNIEnv *env, jobject instance, jstring data,jbyteArray array) {
int len = env->GetArrayLength(array);
const char *utfChars = env->GetStringUTFChars(data, 0);
jbyte *arrayElements = env->GetByteArrayElements(array, NULL);
jstring pJstring = env->NewStringUTF(utfChars);
jbyteArray jpicArray = env->NewByteArray(len);
env->SetByteArrayRegion(jpicArray, 0, len, arrayElements);
// TODO
env->DeleteLocalRef(pJstring);
env->DeleteLocalRef(jpicArray);
env->ReleaseStringUTFChars(data, utfChars);
env->ReleaseByteArrayElements(array, arrayElements, 0);
std::string hello = "Hello from C++";
jstring result = env->NewStringUTF(hello.c_str());
return result;
}
其它的还有:
jclass ref= (env)->FindClass("java/lang/String");
env->DeleteLocalRef(ref);
因为根据jni.h
里的定义:
typedef jobject jclass;
jclass也是jobject。而jmethodID
/jfielID
和jobject没有继承关系,它们不是object,只是个整数,不存在被释放与否的问题。
6.3 局部引用和全局引用的转换
注意Local Reference的生命周期,如果在Native中需要长时间持有一个Java对象,就不能使用将jobject存储在Native,否则在下次使用的时候,即使同一个线程调用,也将会无法使用。下面是错误的做法:
jstring global;
extern "C" JNIEXPORT jstring JNICALL
Java_org_hik_libyuv_MainActivity_stringFromJNI(JNIEnv *env,jobject /* this */) {
std::string hello = "Hello from C++";
jstring local = env->NewStringUTF(hello.c_str());
global = local;
return local;
}
正确的做法是使用Global Reference,如下:
jstring global;
extern "C" JNIEXPORT jstring JNICALL
Java_org_hik_libyuv_MainActivity_stringFromJNI(JNIEnv *env,jobject /* this */) {
std::string hello = "Hello from C++";
jstring local = env->NewStringUTF(hello.c_str());
global = static_cast<jstring>(env->NewGlobalRef(global));
return local;
}
6.4 多线程
JNIEnv和jobject对象都不能跨线程使用。 对于jobject,解决办法是:
a、m_obj = env->NewGlobalRef(obj);//创建一个全局变量
b、jobject obj = env->AllocObject(m_cls);//在每个线程中都生成一个对象
对于JNIEnv,解决办法是在每个线程中都重新生成一个env
JavaVM *gJavaVM;//声明全局变量
(*env)->GetJavaVM(env, &gJavaVM);//在JNI方法的中赋值
JNIEnv *env;//在其它线程中获取当前线程的env
m_jvm->AttachCurrentThread((void **)&env, NULL);
当在一个线程里面调用AttachCurrentThread后,如果不需要用的时候一定要DetachCurrentThread,否则线程无法正常退出,导致JNI环境一直被占用。
七、手动释放内存
7.1 那些需要手动释放?
·不需要手动释放(基本类型):jint,jlong,jchar,jdouble等
·需要手动释放(引用类型,数组家族):jstring,jobject,jobjectArray,jintArray,jclass等
7.2 释放方法
7.2.1 jstring&char*
// 创建 jstring 和 char*
jstring jstr = (*jniEnv)->CallObjectMethod(jniEnv, mPerson, getName);
char* cstr = (char*) (*jniEnv)->GetStringUTFChars(jniEnv,jstr, 0);
// 释放
(*jniEnv)->ReleaseStringUTFChars(jniEnv, jstr, cstr);
(*jniEnv)->DeleteLocalRef(jniEnv, jstr);
7.2.2 jobject,jobjectArray,jclass 等引用类型
(*jniEnv)->DeleteLocalRef(jniEnv, XXX);
7.2.3 jbyteArray
jbyteArray audioArray = jnienv->NewByteArray(frameSize);
jnienv->DeleteLocalRef(audioArray);
7.2.4 GetByteArrayElements
jbyte* array= (*env)->GetByteArrayElements(env,jarray,&isCopy);
(*env)->ReleaseByteArrayElements(env,jarray,array,0);
7.2.5 NewGlobalRef
jobject ref= env->NewGlobalRef(customObj);
env->DeleteGlobalRef(customObj);
7.3 避免内存泄露
JNI如果创建以上引用却不手动释放的话很容易就造成内存泄露,所以JNI编程创建的引用类型一定要手动释放(切身教训),检测内存泄露可以使用Jprofiler