JNI内存管理及优化

栏目: Java · 发布时间: 7年前

内容简介:上面这张图大家都应该很熟了,下面只讲下和JNI有关的部分记录正在执行的虚拟机字节码指令的地址(如果正在执行的是本地方法则为空)。本地方法栈与 Java 虚拟机栈类似,它们之间的区别只不过是本地方法栈为本地方法服务。 本地方法一般是用其它语言(C、C++ 或汇编语言等)编写的,并且被编译为基于本机硬件和操作系统的程序,对待这些方法需要特别处理。
JNI内存管理及优化

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

程序计数器

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

本地方法栈

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

JNI内存管理及优化

堆(Java-Heap)

所有对象都在这里分配内存,是垃圾收集的主要区域("GC 堆")。 堆不需要连续内存,并且可以动态增加其内存,增加失败会抛出 OutOfMemoryError 异常。

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

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的主要作用如下:

  • 管理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中。

举个例子,如下代码:

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 Reference、Global Reference、Weak Global Reference。下面分别来介绍一下这三种引用内存分配和管理。

Local Reference

只在Native Method执行时存在。它的生命期是在Native Method的执行期开始创建(从Java代码切换到Native代码环境时,或者在Native Method执行时调用JNI函数时),在Native Method执行完毕切换回Java代码时,所有Local Reference被删除,生命期结束(调用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 对象的关系

JNI内存管理及优化

接下来举个简单例子说明一下:

jstring jstr = env->NewStringUTF("Hello World!");
复制代码
  • jstr存放在Native Method Stack中,是一个局部变量
  • 对于开发者来说,Local Reference Table是透明的
  • Local Reference Table的内存不大,所能存放的Local Reference数量也是有限的,使用不当就会引起OOM异常
  • Local Reference并不是Native里面的局部变量,局部变量存放在堆栈中,而Local Reference存放在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);
复制代码

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

多线程

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


以上所述就是小编给大家介绍的《JNI内存管理及优化》,希望对大家有所帮助,如果大家有任何疑问请给我留言,小编会及时回复大家的。在此也非常感谢大家对 码农网 的支持!

查看所有标签

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。

Web Security Testing Cookbook

Web Security Testing Cookbook

Paco Hope、Ben Walther / O'Reilly Media / 2008-10-24 / USD 39.99

Among the tests you perform on web applications, security testing is perhaps the most important, yet it's often the most neglected. The recipes in the Web Security Testing Cookbook demonstrate how dev......一起来看看 《Web Security Testing Cookbook》 这本书的介绍吧!

JS 压缩/解压工具
JS 压缩/解压工具

在线压缩/解压 JS 代码

SHA 加密
SHA 加密

SHA 加密工具