Fork me on GitHub

JNI相关使用记录

JNI 工作流程

  1. java层调用system.load方法。
  2. 通过classloader拿到了so文件的绝对路径,然后调用nativeload()方法。
  3. 通过linux下的dlopen方法,加载并查找so库里的方法。
  4. 当前线程下的 JNIENV 会将所有的jni方法注册到了同一个Jvm中,so和class到了同一个进程空间
    (人脸项目中就是在Strom的一个Worker JVM,多个executor线程共享一个faceEengine对象)(JNIENV 代表了java在本线程的运行环境,每个线程都有一个)。
  5. 通过当前线程的jnienv即可调用对应的对象方法了。

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执行完毕切换回Java代码时,JVM发现没有JAVA层引用,Local Reference被JVM回收并释放,所有Local Reference被删除,生命期结束;
  • 或调用DeleteLocalRef可以提前结束其生命期。
  • 局部引用只对当前线程有效,多个线程间不能共享局部引用。
  • 基于谁创建谁销毁的原则,native函数执行完后,局部引用没有被native代码显示删除,那么局部引用在JVM中还是有效的,JVM决定什么时候删除它,和C语言的局部变量含义是不一样的。

每当线程从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数量默认16个是有限的,使用不当就会引起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);
}

相关Tips和优化

  • 在jni_onload初始化全局引用和弱全局引用;

  • jobject默认是local Ref,函数环境消失时会跟随消失

  • C++调用java需要查找类,查找方法,查找方法ID,获取字段或者方法的调用有时候会需要在JVM中完成大量工作,因为字段和方法可能是从超类中继承而来的,为特定类返回的id不会在Jvm进程生存期间发生变化 ,这会让jvm向上遍历类层次结构来找到它们,这是开销很大的操作。
    所以,缓存ID字段是为了降低CPU负载,提高运行速度。

    1. jmethodID/jfielID 和 jobject 没有继承关系,它不是个object,只是个整数,不存在被释放与否的问题,可用全局变量保存。
    2. jclass、jstring是由jobject继承而来的类,所以它是个jobject,需要用全局变量保存。
  • 局部引用管理new出来的对象,注意及时delete。总体原则,注意释放所有对jobject的引用。

  • 不同线程使用JNIEnv*对象,需要AttachCurrentThread将env挂到当前线程,否则无法使用env。

  • 尽量避免频繁调用JNI或者是使用JNI传输大量到数据。

开发相关

二维数组

二维数组具有特殊性在于,可以将它看成一维数组,其中数组的每项内容又是一维数组。

参考

  • 基础知识:
  1. JNI内存管理
  • 开发相关:
  1. Java层与Jni层的数组传递
  2. jbytearray与 C++Byte数组之间的转换
  3. JNI调用时缓存字段和方法 ID
  4. how to cache classId
posted @ 2019-07-25 10:21  stillcoolme  阅读(763)  评论(0编辑  收藏  举报