前端开发入门到精通的在线学习网站

网站首页 > 资源文章 正文

JNI 内存管理及优化(内存优化表)

qiguaw 2024-10-01 15:08:49 资源文章 15 ℃ 0 评论

一、Java内存与Native 内存

奉上一张内存分布图先,本文只讨论JNI 的内存分布与管理以及分析它是如何在Java 层与Native层担任中间人角色供Java与Native 通讯的,其他的请自行查阅相关资料。

1.1 本地方法栈

本地方法栈与 Java 虚拟机栈类似,它们之间的区别只不过是本地方法栈为本地方法服务。 本地方法一般是用其它语言(C/C++ 或汇编语言等)编写的,并且被编译为基于本机硬件和操作系统的程序,对待这些方法需要特别处理。

1.2 直接内存(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中的。

1.3 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中。它所处的层级关系如下图所示:

二、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中。

JNI引用有三种:Local ReferenceGlobal ReferenceWeak Global Reference。下面分别来介绍一下这三种引用内存分配和管理。

2.1Local 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 Table、Local Reference 和 Java 对象的关系如下所示:



我们知道局部变量在函数栈运行结束时就会自动回收,这一点不论是Java 语言还C/C++ 语言机制都是一样的。只要作用域一结束操作系统就自动将局部变量所占的内存空间进行回收,不需要开发者手动管理。

现在JNI 中引入了Local Reference ,按照常规理解既然在函数作用域结束后会回收Local Reference,是不是就不需要关注Local Reference 内存分配呢?百说不如一练,写段代码试试:

例子1:

extern "C"
JNIEXPORT void JNICALL
Java_come_live_ndkdemo_NativeTest_testJNIMem(JNIEnv *env, jobject thiz) {
    jint i = 0;
    jstring str;
    for(; i<100000000; i++) {
        str = env->NewStringUTF("0");
    }

}

大写的尴尬,居然出现局部引用表溢出,这是为啥呢?

上述这段代码很简单,就是循环执行 100000000 次,JNI function NewStringUTF() 在每次循环中从 Java Heap 中创建一个 String 对象,str 是 Java Heap 传给 JNI native method 的 Local Reference,每次循环中新创建的 String 对象覆盖上次循环中 str 的内容。str 似乎一直在引用到一个 String 对象。整个运行过程中,看似只创建一个 Local Reference,实则不然,请见下述分析

2.2 Local Reference 的深入理解

前面的介绍Local Reference 提到 JNI Local Reference 的生命期是在 native method 的执行期(从 Java 程序切换到 native code 环境时开始创建,或者在 native method 执行时调用 JNI function 创建),在 native method 执行完毕切换回 Java 程序时,所有 JNI Local Reference 被删除,生命期结束(调用 JNI function 可以提前结束其生命期)。

实际上,每当线程从 Java 环境切换到 native code 上下文时(J2N),JVM 会分配一块内存,创建一个 Local Reference 表,这个表用来存放本次 native method 执行中创建的所有的 Local Reference。每当在 native code 中引用到一个 Java 对象时,JVM 就会在这个表中创建一个 Local Reference。比如,例子 1 中我们调用 NewStringUTF() 在 Java Heap 中创建一个 String 对象后,在 Local Reference 表中就会相应新增一个 Local Reference,现在循环体执行了100000000次,就在Java Heap 中创建了100000000 对象,相应的在Local Reference Table 就创建100000000 个Local Reference ,因为Local Reference Table的内存不大,所能存放Local Reference 数量也是有限的(JNI ERROR (app bug): local reference table overflow (max=8388608) 测试程序使用的设备为Android 9.0 )使用不当就会引起溢出异常。

实例1 中:

 str = env->NewStringUTF("0");

str 是 jstring 类型的局部变量。Local Ref 表中会新创建一个 Local Reference,引用到 NewStringUTF( "0") 在 Java Heap 中新建的 String 对象。如下图 所示:



  1. 运行 native method 的线程的堆栈记录着 Local Reference 表的内存位置(指针 p)。
  2. Local Reference 表中存放 JNI Local Reference,实现 Local Reference 到 Java 对象的映射。
  3. native method 代码间接访问 Java 对象(java obj1,java obj2)。通过指针 p 定位相应的 Local Reference 的位置,然后通过相应的 Local Reference 映射到 Java 对象。
  4. 当 native method 引用一个 Java 对象时,会在 Local Reference 表中创建一个新 Local Reference。在 Local Reference 结构中写入内容,实现 Local Reference 到 Java 对象的映射。
  5. native method 调用 DeleteLocalRef() 释放某个 JNI Local Reference 时,首先通过指针 p 定位相应的 Local Reference 在 Local Ref 表中的位置,然后从 Local Ref 表中删除该 Local Reference,也就取消了对相应 Java 对象的引用(Ref count 减 1)。
  6. 当越来越多的 Local Reference 被创建,这些 Local Reference 会在 Local Ref 表中占据越来越多内存。当 Local Reference 太多以至于 Local Ref 表的空间被用光,JVM 会抛出异常,从而导致 JVM 的崩溃。

上图中,str 是局部变量,在 native method 堆栈中。Local Ref3 是新创建的 Local Reference,在 Local Ref 表中,引用新创建的 String 对象。JNI 通过 str 和指针 p 间接定位 Local Ref3,但 p 和 Local Ref3 对 JNI 程序员不可见。

局部变量 str 在每次循环中都被重新赋值,间接指向最新创建的 Local Reference,前面创建的 Local Reference 一直保留在 Local Ref 表中。

在实例 1 执行完第 i 次循环后,内存布局如下图 :



继续执行完第 i+1 次循环后,内存布局发生变化,如下图:



上图 中,局部变量 str 被赋新值,间接指向了 Local Ref i+1。在 native method 运行过程中,我们已经无法释放 Local Ref i 占用的内存,以及 Local Ref i 所引用的第 i 个 string 对象所占据的 Java Heap 内存。所以,native memory 中 Local Ref i 被泄漏,Java Heap 中创建的第 i 个 string 对象被泄漏了。也就是说在循环中,前面创建的所有 i 个 Local Reference 都泄漏了 native memory 的内存,创建的所有 i 个 string 对象都泄漏了 Java Heap 的内存。直到 native memory 执行完毕,返回到 Java 程序时(N2J),这些泄漏的内存才会被释放,但是 Local Reference 表所分配到的内存往往很小,在很多情况下 N2J 之前可能已经引发严重内存泄漏,导致 Local Reference 表的内存耗尽,使 JVM 崩溃。实例1中的正确的做法是,在作用域时未结束时同时为了LocalReference Table 未使用完时手动将Local Reference 删除。代码如下:

extern "C"
JNIEXPORT void JNICALL
Java_come_live_ndkdemo_NativeTest_testJNIMem(JNIEnv *env, jobject thiz) {
    jint i = 0;
    jstring str;
    for(; i<100000000; i++) {
        str = env->NewStringUTF("0");
        env->DeleteLocalRef(str);
    }

}

Tips 1:

在 JNI 编程时,正确控制 JNI Local Reference 的生命期。如果需要创建过多的 Local Reference,那么在对被引用的 Java 对象操作结束后,需要调用 JNI function(如 DeleteLocalRef()),及时将 JNI Local Reference 从 Local Ref 表中删除,以避免潜在的内存泄漏。

Tips 2:

Local Reference 不是 native code 的局部变量

Native Code 的局部变量和 Local Reference 是完全不同的,区别可以总结为:

  1. 局部变量存储在线程堆栈中,而 Local Reference 存储在 Local Ref 表中。
  2. 局部变量在函数退栈后被删除,而 Local Reference 在调用 DeleteLocalRef() 后才会从 Local Ref 表中删除,并且失效,或者在整个 Native Method 执行结束后被删除。
  3. 可以在代码中直接访问局部变量,而 Local Reference 的内容无法在代码中直接访问,必须通过 JNI function 间接访问。JNI function 实现了对 Local Reference 的间接访问,JNI function 的内部实现依赖于具体 JVM。

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

2.3、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回收。

三、JNI 内存释放规则

3.1 需要释放的类型区别

  1. 基本类型不需要手动释放,如jint,jlong,jchar,jdouble等
  2. 引用类型需要手动释放(引用类型,数组家族):jstring,jobject,jobjectArray,jintArray,jclass等

3.2 释放的方法

3.2.1 jstring&char*

// C 方式
// 创建 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);

// C++ 方式
// 创建 jstring 和 char*
jstring jstr = jniEnv->CallObjectMethod( mPerson, getName);
char* cstr = (char*) jniEnv->GetStringUTFChars(jstr, 0);
 
// 释放 
jniEnv->ReleaseStringUTFChars(jstr, cstr);
jniEnv->DeleteLocalRef(jstr);

3.2.2 jobject,jobjectArray,jclass 等引用类型

jniEnv->DeleteLocalRef( XXX);

3.2.3 jbyteArray

jbyteArray audioArray = jnienv->NewByteArray(frameSize);
jnienv->DeleteLocalRef(audioArray);

3.2.4 GetByteArrayElements

jbyte* array= (*env)->GetByteArrayElements(env,jarray,&isCopy);
(*env)->ReleaseByteArrayElements(env,jarray,array,0);

3.2.5 NewGlobalRef

jobject ref= env->NewGlobalRef(customObj);
env->DeleteGlobalRef(customObj);

3.2.6 示例:

错误?:没有正确释放,会导致内存泄漏

const char *str = env->GetStringUTFChars(jstr, nullptr);

正确?:必须调用 ReleaseStringUTFChars 释放

const char *str = env->GetStringUTFChars(jstr, nullptr);// TODO use str
env->ReleaseStringUTFChars(jstr, str);

错误?:Release 之后就不能再使用

const char *str = env->GetStringUTFChars(jstr, nullptr);
env->ReleaseStringUTFChars(jstr, str);// TODO use str

正确?:可以把 char* 转换成 std::string再使用

const char *c_str = env->GetStringUTFChars(jstr, nullptr);
std::string str(c_str, env->GetStringLength(jstr));
env->ReleaseStringUTFChars(jstr, c_str);// TODO use str

正确?:自己分配空间,自己进行 delete 释放。如果数据不大,推荐先转换成 std::string。

int size = env->GetStringLength(jstr);char *c_arr = new char[size];
env->GetStringRegion(jstr, 0, size, (jchar *) c_arr);// TODO use c_arrdelete[] c_arr;

获取数组数据

错误?:没有正确释放,会导致内存泄漏

auto int_arr = env->GetIntArrayElements(jint_arr, nullptr);

正确?

auto int_arr = env->GetIntArrayElements(jint_arr, nullptr);// TODO use int_arr
env->ReleaseIntArrayElements(jint_arr, int_arr, 0);

错误?:不能在 Release 之后使用,会导致野指针。

auto int_arr = env->GetIntArrayElements(jint_arr, nullptr);
env->ReleaseIntArrayElements(jint_arr, int_arr, 0);// TODO use int_arr

正确?:如果需要在 Release 之后使用,那么就要自行分配内存,然后拷贝,但是自己分配的内存也要释放。

int size = env->GetArrayLength(jint_arr);auto int_arr = env->GetIntArrayElements(jint_arr, nullptr);int *int_arr2 = new int[size];memcpy(int_arr2, int_arr, sizeof(int) * size);
env->ReleaseIntArrayElements(jint_arr, int_arr, 0);// TODO use int_arrdelete [] int_arr2;

正确?:自己分配内存,使用 GetIntArrayRegion 拷贝数据,无需调用 ReleaseIntArrayElements 函数。但是也要注意释放自己分配的空间。

int size = env->GetArrayLength(jint_arr);int *int_arr = new int[size];
env->GetIntArrayRegion(jint_arr, 0, size, int_arr);// TODO use int_arrdelete[] int_arr;

基本原则

  1. GetStringUTFChars 和 ReleaseStringUTFChars,GetXXArrayElements 和 ReleaseXXArrayElements,必须对应起来,否则会导致内存泄漏。
  2. GetXXArrayElements 生成的数据不能在 ReleaseXXArrayElements 之后使用。
  3. 如果是在JNI函数内通过NewStringUTF、NewXXXArray或NewObject创建的java对象无论是否需要返回java层,都不需要手动释放,jvm会自动管理。但是如果是通过 AttachCurrentThread 创建的 JNIEnv 去New的对象,必须通过 DeleteLocalRef 方式及时删除,因为在线程销毁之前,创建的对象无法自动回收(需要注意的点是 Local Ref 需要及时删除 防止Local Ref Table 溢出)。
  4. 通过 NewGlobalRef 创建的对象必须手动释放。
  5. FindClass 和 GetMethodID 不需要释放。
  6. 如果不是通过 NewGlobalRef 函数创建的java对象不能跨线程调用,jclass 也是 jobject,如果是在 JNI_OnLoad 创建,那么必须通过 NewGlobalRef 函数处理后才能正常使用。

问题 参考链接:https://www.jianshu.com/p/5cde114159d4

GetStringUTFChars 和 GetStringRegion,GetByteArrayElements 和 GetByteArrayRegion 区别

GetStringUTFChars 和 GetByteArrayElements 函数都是jni帮我们分配内存然后拷贝jvm对象的信息到native层,需要通知jni去释放内存。

GetStringRegion 和 GetByteArrayRegion 是我们自己分配好内存,然后把指针传给jni,jni往指针写入数据,所以不需要jni去释放内存,但是我们自己分配的空间不需要使用后必须释放。

四、多线程环境下使用

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);  

//不用的时候 需要
m_jvm->DetachCurrentThread((void **)&env, NULL);  

当在一个线程里面调用AttachCurrentThread后,如果不需要用的时候一定要DetachCurrentThread,否则线程无法正常退出,导致JNI环境一直被占用。


参考文档:

https://blog.csdn.net/a2241076850/article/details/81091705

https://blog.csdn.net/nanke_yh/article/details/124863685

https://www.jianshu.com/p/5cde114159d4

Tags:

本文暂时没有评论,来添加一个吧(●'◡'●)

欢迎 发表评论:

最近发表
标签列表