zoukankan      html  css  js  c++  java
  • 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
  • 相关阅读:
    2013年工作总结
    jquery的ajax向后台servlet传递json类型的多维数组
    web客户端通过ajaxfileupload方式上传文件
    servlet向ajax传递list数据类型,ajax向servlet传递array数据类型
    simplemodal — jquery弹出窗体插件
    【摘抄】活在当下
    unix网络编程 str_cli epoll 非阻塞版本
    unix网络编程str_cli使用epoll实现
    unix网络编程str_cli的epoll实现
    linux 建议锁和强制锁
  • 原文地址:https://www.cnblogs.com/stillcoolme/p/11242602.html
Copyright © 2011-2022 走看看