JNI内存管理及优化

JVM内存和Native内存

上面这张图你们都应该很熟了,下面只讲下和JNI有关的部分html

程序计数器

记录正在执行的虚拟机字节码指令的地址(若是正在执行的是本地方法则为空)。java

本地方法栈

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

堆(Java-Heap)

全部对象都在这里分配内存,是垃圾收集的主要区域("GC 堆")。 堆不须要连续内存,而且能够动态增长其内存,增长失败会抛出 OutOfMemoryError 异常。git

能够经过 -Xms 和 -Xmx 两个虚拟机参数来指定一个程序的堆内存大小,第一个参数设置初始值,第二个参数设置最大值。github

java -Xmx1024m -Xms1024m
//-Xmx1024m:设置JVM最大可用内存为1024M。
//-Xms1024m:设置JVM初始内存为1024m。此值可与-Xmx相同,以免每次垃圾回收完成后JVM从新分配内存。
复制代码

在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的主要做用以下:bash

  • 管理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回收内存。app

 然而,JNI和上面二者又有些区别。 JNI提供了与Java相对应的引用类型(如:jobject、jstring、jclass、jarray、jintArray等),以便Native代码能够经过JNI函数访问到Java对象。引用所指向的Java对象一般就是存放在Java Heap,而Native代码持有的引用是存放在Native Memory中。jvm

举个例子,以下代码:

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

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内存泄漏。
复制代码

JNI引用

JNI引用有三种:Local ReferenceGlobal ReferenceWeak 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回收。

注意点

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的引用:

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,只是个整数,不存在被释放与否的问题。

局部引用和全局引用的转换

注意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;
}
复制代码

多线程

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环境一直被占用。

参考文章

C++调用JAVA方法详解

JNI内存管理

Java 虚拟机

相关文章
相关标签/搜索