JNI内存管理

一、Java内存 


 Java程序所涉及的内存可以从逻辑上划分为两部分:Heap MemoryNative Memory

1)Heap Memory

 供Java应用程序使用的,所有java对象的内存都是从这里分配的,它不是物理上连续的,但是逻辑上是连续的。可通过java命令行参数“-Xms, -Xmx”大设置Heap初始值和最大值。
java -Xmx1024m -Xms1024m
//-Xmx1024m:设置JVM最大可用内存为1024M//-Xms1024m:设置JVM初始内存为1024m。此值可与-Xmx相同,以避免每次垃圾回收完成后JVM重新分配内存。

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的主要作用如下:
  1. 管理java heap的状态数据(用于GC);
  2. JNI调用,也就是Native Stack;
  3. JIT(即使编译器)编译时使用Native Memory,并且JIT的输入(Java字节码)和输出(可执行代码)也都是保存在Native Memory;
  4. NIO direct buffer;
  5. Threads;
  6. 类加载器和类信息都是保存在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     


 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。

 代码如下:
jstring jstr = env->NewStringUTF("Hello World!");
 示意图如下:
【JNI开发】11、JNI开发笔记-内存管理

    1)jstr存放在Native Method Stack中,是一个局部变量
    2)对于开发者来说,Local Reference Table是透明的
    3)Local Reference Table的内存不大,所能存放的Local Reference数量也是有限的,使用不当就会引起OOM异常
    4)Local Reference并不是Native里面的局部变量,局部变量存放在堆栈中,而Local Reference存放在Local Reference Table中。

    在Native Method结束时,JVM会自动释放Local Reference,但在开发中,应该及时使用DeleteLocalRef()删除不必要的Local Reference,避免Local Reference Table被撑破,下面举个例子:

C++代码:
#include "jni.h"
#include "android/log.h"

void onInit(JNIEnv* env);

//该函数在库被加载的时候执行
JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM *jvmvoid *reserved) {
    JNIEnv *env = NULL;
    jint result = -1;
    if (jvm->GetEnv((void **) &envJNI_VERSION_1_4) != JNI_OK) {
        return -1;
    }
    onInit(env);
    result JNI_VERSION_1_4;
    return result;
}

void onInit(JNIEnv* env){
    jstring jstr;

    for(int 010000i++){
        jstr = env->NewStringUTF("Hello World!");
        __android_log_print(ANDROID_LOG_DEBUG"testabc""i=%d"i);
    }
}


运行情况:
D/testabc: i=501
D/testabc: i=502
D/testabc: i=503
D/testabc: i=504
A/art:  JNI ERROR (app bug): local reference table overflow (max=512)
A/art:  local reference table dump:
        A/art:    Last 10 entries (of 512):
A/art:        5110x12d86a90 java.lang.String "Hello World!"
A/art:        5100x12d86a60 java.lang.String "Hello World!"
A/art:        5090x12d86a30 java.lang.String "Hello World!"
A/art:        5080x12d86a00 java.lang.String "Hello World!"
A/art:        5070x12d869d0 java.lang.String "Hello World!"
A/art:        5060x12d869a0 java.lang.String "Hello World!"
A/art:        5050x12d86970 java.lang.String "Hello World!"
A/art:        5040x12d86940 java.lang.String "Hello World!"
A/art:        5030x12d86910 java.lang.String "Hello World!"
A/art:        5020x12d868e0 java.lang.String "Hello World!"
A/art:    Summary:
A/art:          of java.lang.Class (unique instances)
A/art:          of java.lang.String[] (elements)
A/art:        508 of java.lang.String (508 unique instances)
A/art:          of dalvik.system.PathClassLoader
        A/art:
        A/art: Check failed: count_ == (count_=-10=0) Attempted to destroy barrier with non zero count
A/art: Runtime aborting --- recursivelyso no thread-specific detail!
A/art:  
    以上代码是在Android手机上运行,通过错误信息可以看到,Native Method还没执行结束,当i=505的时候,就出现异常了local reference table overflow (max=512),这段代码看似只有一个jstr局部变量,实际上在每次调用NewStringUTF()的时候,都会在Local Reference Table中创建了一个Local Reference,在崩溃出现时,Local Reference Table中已经有512个Local Reference了,再次创建的时候,就超出了Local Reference Table的最大允许数量了,于是JVM就调用了FatalError(看错误信息,Local Reference最大数目应该是512才对啊,为什么505就报错了?这是因为Native方法参数等还占用了一些,见上面信息中的Summary部分

 为了避免出现以上错误,在用完Local Reference时,很有必要调用DeleteLocalRef()将其删除掉。
/**
 * 删除localRef所指向的局部引用。
 * @localRef localRef:局部引用
*/
voi DeleteLocalRef(jobject localRef);
 DeleteLocalRef()的参数是一个jobject引用类型,对于一般的基本数据类型(如:jint,jdouble等),是没必要调用该函数删除掉的,但是像jstring、jintArray、jobject这些就需要了。

 注意Local Reference的生命周期,如果在Native中需要长时间持有一个Java对象,就不能使用将jobject存储在Native,否则在下次使用的时候,即使同一个线程调用,也将会无法使用。下面是错误的做法:
class MyPeer {
public:
    MyPeer(jstring s) {
        str_ = s// Error: stashing a reference without ensuring it’s global.
    }
    jstring str_;
};

static jlong MyClass_newPeer(JNIEnv* envjclass) {
    jstring local_ref = env->NewStringUTF("hello, world!");
    MyPeerpeer new MyPeer(local_ref);
    return static_cast<jlong>(reinterpret_cast<uintptr_t>(peer));
    // Error: local_ref is no longer valid when we return, but we've stored it in 'peer'.
}

static void MyClass_printString(JNIEnv* envjclassjlong peerAddress) {
    MyPeer* peer = reinterpret_cast<MyPeer*>(static_cast<uintptr_t>(peerAddress));
    // Error: peer->str_ is invalid!
    ScopedUtfChars s(envpeer->str_);
    std::cout << s.c_str() << std::endl;
}
 正确的做法是使用Global Reference,如下:
class MyPeer {
public:
    MyPeer(JNIEnv* envjstring s) {
        this->s = env->NewGlobalRef(s);
    }
    ~MyPeer() {
        assert(== NULL);
    }
    void destroy(JNIEnv* env) {
        env->DeleteGlobalRef(s);
        = NULL;
    }
    jstring s;
};

 下面来介绍一下Global Reference。

四、Global Reference


    在理解了Local Reference之后,再来理解Global Reference和Weak Global Reference就简单多了
    Global Reference与Local 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 ReferenceNewWeakGlobalRef()DeleteWeakGlobalRef()进行创建和删除,它与Global Reference的区别在于该类型的引用随时都可能被GC回收。

    对于Weak Global Reference而言,可以通过isSameObject()将其与NULL比较,看看是否已经被回收了。如果返回JNI_TRUE,则表示已经被回收了,需要重新初始化弱全局引用。
/**
 * 判断两个引用是否引用同一Java对象。
 * @param ref1 Java对象
 * @param ref2 Java对象
 * @retrun 如果ref1ref2引用同一Java对象或均为 NULL,则返回 JNI_TRUE。否则返回 JNI_FALSE
*/
jboolean IsSameObject(jobject ref1jobject ref2);

    Weak Global Reference的回收时机是不确定的,有可能在前一行代码判断它是可用的,后一行代码就被GC回收掉了。为了避免这事事情发生,JNI官方给出了正确的做法,通过NewLocalRef()获取Weak Global Reference,避免被GC回收。示例代码如下:
static jobject weakRef = NULL;

JNIEXPORT void JNICALL Java_com_bassy_jnitest_Main_getName(JNIEnv *envjobject instance) {

    jobject localRef;
    //We ensure create localRef success        while(weakRef == NULL || (localRef = env->NewLocalRef(weakRef)) == NULL){
        //Init weak global reference again
        weakRef = env->NewWeakGlobalRef(...)
    }
    //Process localRef
    //...
    env->DeleteLocalRef(localRef);
}


  • 1
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值