【转载】JNI内存方面说明以及相关类型手动释放内存

一、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

转载来源:https://developer.aliyun.com/article/1112357

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,937评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,503评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,712评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,668评论 1 276
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,677评论 5 366
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,601评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,975评论 3 396
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,637评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,881评论 1 298
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,621评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,710评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,387评论 4 319
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,971评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,947评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,189评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 44,805评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,449评论 2 342

推荐阅读更多精彩内容

  • jni中的数据传递就两种:c层传到java层;java层传到c层。 1 当数据从java传递到c 1.1 传递基本...
    HWilliamgo阅读 1,835评论 0 0
  • 闲来翻译了一篇官方的JNI Tips,网上看到的翻译版本要么是时间久了不同步了,要么翻译的过于生硬,看得我怀疑自己...
    生活简单些阅读 1,719评论 1 4
  • JNI代码实践 [TOC] 说明 关于jni代码的cmake构建脚本,kotlin如何声明和使用native方法,...
    云佾风徽阅读 2,307评论 0 1
  • 转自:https://www.ibm.com/developerworks/cn/java/j-lo-jnilea...
    十七17阅读 1,437评论 0 7
  • Java 与JNI 内存管理是怎样的想要弄清楚Java与JNI的内存管理的关系,首先要弄清楚JVM的内存模型 其中...
    csong阅读 3,272评论 0 6