JNI相关使用记录

JNI 内存模型

Java应用程序所涉及的内存可以从逻辑上划分为两部分:Heap Memory和Native Memory。

Java应用程序都是在Java Runtime Environment(JRE)中运行,而JRE本身就是由Native语言(如:C/C++)编写的程序。
(JVM只是JRE的一部分,JVM的内存模型属于另一话题)

所以包含关系大致这样:(JRE (JVM (Heap Mem, Native Memory) ) )

  1. Heap Memory:供Java应用程序使用,所有java对象的内存都是从这里分配的,它物理上不连续,但是逻辑上是连续的。可通过java命令行参数“-Xms, -Xmx”大设置Heap初始值和最大值。
  2. Native Memory:Java Runtime进程使用,没有相应的参数来控制其大小,由操作系统分配给Runtime进程的可用内存,大小依赖于操作系统进程的最大值。

Native Memory的主要作用如下:

  • 管理java heap的状态数据(用于GC);
  • JNI调用,也就是Native Stack,JNI内存分配其实与Native Memory有很大关系
  • JIT编译时使用Native Memory,并且JIT的输入(Java字节码)和输出(可执行代码)也都是保存在Native Memory;
  • NIO direct buffer;
  • 线程资源。

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异常和内存泄露,我们在进行JNI开发的时候,需要熟悉它的内存分配和管理。

JNI引用类型

JNI引用类型有三种:Local Reference、Global Reference、Weak Global Reference。

下面分别来介绍一下这三种引用内存分配和管理。

Local Reference

Local Reference只在Native Method执行时存在。

它的生命期是在Native Method的执行期开始创建,在Native Method执行完毕切换回Java代码时,所有Local Reference被删除,生命期结束(调用DeleteLocalRef可以提前结束其生命期)。

每当线程从Java环境切换到Native代码环境时,JVM 会分配一块内存用于创建一个Local Reference Table,这个Table用来存放本次Native Method 执行中创建的所有Local Reference,所以Local Reference不属于Native Code 的局部变量

每当在 Native代码中引用到一个Java对象时,JVM 就会在这个Table中创建一个Local Reference

比如jstring jstr = env->NewStringUTF("Hello World!");,我们调用 NewStringUTF() 在 Java Heap 中创建一个 String 对象后,在 Local Reference Table 中就会相应新增一个 Local Reference。

Local Reference示意图

  • jstr存放在Native Method Stack中,是一个局部变量;
  • 然后通过 Local Reference Table中的 localRef 指向 Heap Mem ,Local Reference Table对我们来说是透明的;
  • Local Reference Table的内存不大,所能存放的Local Reference数量是有限的,使用不当就会引起OOM异常
  • 在Native Method结束时,JVM会自动释放Local Reference,但在开发中如果循环中或其他情况创建大量Local; Reference,应该及时使用DeleteLocalRef()删除不必要的Local Reference,避免Local Reference Table被撑破。
  • Local Reference并不是Native里面的局部变量,局部变量存放在堆栈中,而Local Reference存放在Local Reference Table中。
  • DeleteLocalRef()的参数是一个jobject引用类型,对于一般的基本数据类型(如:jint,jdouble等),是没必要调用该函数删除掉的,但是像jstring、jintArray、jobject这些就需要了。
/**
 * 删除localRef所指向的局部引用。
 * @localRef localRef:局部引用
*/
voi DeleteLocalRef(jobject localRef);

 

注意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* env, jclass) {
    jstring local_ref = env->NewStringUTF("hello, world!");
    MyPeer* peer = 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* env, jclass, jlong peerAddress) {
    MyPeer* peer = reinterpret_cast<MyPeer*>(static_cast<uintptr_t>(peerAddress));
    // Error: peer->str_ is invalid!
    ScopedUtfChars s(env, peer->str_);
    std::cout << s.c_str() << std::endl;
}

正确的做法是使用Global Reference,如下:

class MyPeer {
public:
    MyPeer(JNIEnv* env, jstring s) {
        this->s = env->NewGlobalRef(s);
    }
    ~MyPeer() {
        assert(s == NULL);
    }
    void destroy(JNIEnv* env) {
        env->DeleteGlobalRef(s);
        s = NULL;
    }
    jstring s;
};

Global Reference

在理解了Local Reference之后,再来理解Global Reference和Weak Global Reference就简单多了。

Global Reference与Local Reference的区别在于生命周期和作用范围:

  1. Global Reference是通过JNI函数NewGlobalRef()和DeleteGlobalRef()来创建和删除的。
  2. 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

Weak Global Reference用NewWeakGlobalRef()和DeleteWeakGlobalRef()进行创建和删除。

它与Global Reference的区别在于该类型的引用随时都可能被GC回收。

对于Weak Global Reference而言,可以通过isSameObject()将其与NULL比较,看看是否已经被回收了。

如果返回JNI_TRUE,则表示已经被回收了,需要重新初始化弱全局引用。

/**
 * 判断两个引用是否引用同一Java对象。
 * @param ref1 Java对象
 * @param ref2 Java对象
 * @retrun 如果ref1和ref2引用同一Java对象或均为 NULL,则返回 JNI_TRUE。否则返回 JNI_FALSE。
*/
jboolean IsSameObject(jobject ref1, jobject 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 *env, jobject 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);
}

参考

JNI内存管理

猜你喜欢

转载自www.cnblogs.com/stillcoolme/p/11242602.html