JNI(二) 基础-原理

在计算机系统中,每一种编程语言都有一个执行环境(Runtime),执行环境用来解释执行语言中的语句。Java语言的执行环境是Java虚拟机(JVM),JVM其实是主机环境中的一个进程,每个JVM虚拟机都在本地环境中有一个JavaVM结构体,该结构体在创建Java虚拟机时被返回,在JNI环境中创建JVM的函数为JNI_CreateJavaVM。

jint JNI_CreateJavaVM(JavaVM**, JNIEnv**, void*);

1. JavaVM

在这里插入图片描述

JavaVM是Java虚拟机在JNI层的代表,JNI全局仅仅有一个JavaVM结构中封装了一些函数指针(或叫函数表结构),JavaVM中封装的这些函数指针主要是对JVM操作接口。

2. JNIEnv

JNIEnv是当前Java线程的执行环境,一个JVM对应一个JavaVM结构,而一个JVM中可能创建多个Java线程,每个线程对应一个JNIEnv结构,它们保存在线程本地存储TLS中。

JNIEnv 表示 Java 调用 native 语言的环境,是一个封装了几乎全部 JNI 方法的指针。

JNIEnv 只在创建它的线程生效,不能跨线程传递,不同线程的 JNIEnv 彼此独立。

native 环境中创建的线程,如果需要访问 JNI,必须要调用 AttachCurrentThread 关联,并使用 DetachCurrentThread 解除链接。

在这里插入图片描述

2.1 JNIEnv和JavaVM的区别:
  • JavaVM:JavaVM是Java虚拟机在JNI层的代表,JNI全局仅仅有一个
  • JNIEnv:JavaVM 在线程中的代码,每个线程都有一个,JNI可能有非常多个JNIEnv;
2.2 JNIEnv的作用:
  • 调用Java 函数:JNIEnv代表了Java执行环境,能够使用JNIEnv调用Java中的代码
  • 操作Java代码:Java对象传入JNI层就是jobject对象,需要使用JNIEnv来操作这个Java对象
2.3 JNIEnv的创建

JNIEnv 创建与释放:从JavaVM获得,这里面又分为C与C++,我们就依次来看下:

  • C 中——JNIInvokeInterface:JNIInvokeInterface是C语言环境中的JavaVM结构体,调用 (AttachCurrentThread)(JavaVM, JNIEnv**, void*) 方法,能够获得JNIEnv结构体
  • C++中 ——_JavaVM:_JavaVM是C++中JavaVM结构体,调用jint AttachCurrentThread(JNIEnv** p_env, void* thr_args) 方法,能够获取JNIEnv结构体;
2.4 JNIEnv的释放
  • C 中释放:调用JavaVM结构体JNIInvokeInterface中的(DetachCurrentThread)(JavaVM)方法,能够释放本线程的JNIEnv
  • C++ 中释放:调用JavaVM结构体_JavaVM中的jint DetachCurrentThread(){ return functions->DetachCurrentThread(this); } 方法,就可以释放 本线程的JNIEnv
2.5 JNIEnv与线程
  • JNIEnv只在当前线程有效:JNIEnv仅仅在当前线程有效,JNIEnv不能在线程之间进行传递,在同一个线程中,多次调用JNI层方便,传入的JNIEnv是同样的
  • 本地方法匹配多个JNIEnv:在Java层定义的本地方法,能够在不同的线程调用,因此能够接受不同的JNIEnv
2.6 JNIEnv结构

JNIEnv是一个指针,指向一个线程相关的结构,线程相关结构,线程相关结构指向JNI函数指针数组,这个数组中存放了大量的JNI函数指针,这些指针指向了详细的JNI函数。

在这里插入图片描述

2.7 与JNIEnv相关的常用函数
2.7.1 创建Java中的对象
  • jobject NewObject(JNIEnv *env, jclass clazz,jmethodID methodID, …);
  • jobject NewObjectA(JNIEnv *env, jclass clazz,jmethodID methodID, const jvalue *args);
  • jobject NewObjectV(JNIEnv *env, jclass clazz,jmethodID methodID, va_list args);

第一个参数jclass class 代表的你要创建哪个类的对象,第二个参数,jmethodID methodID代表你要使用那个构造方法ID来创建这个对象。只要有jclass和jmethodID,我们就可以在本地方法创建这个Java类的对象。

2.7.2 创建Java类中的String对象
  • jstring NewString(JNIEnv *env, const jchar *unicodeChars,jsize len);

通过Unicode字符的数组来创建一个新的String对象。 env是JNI接口指针;unicodeChars是指向Unicode字符串的指针;len是Unicode字符串的长度。返回值是Java字符串对象,如果无法构造该字符串,则为null。

2.7.3 创建类型为基本类型PrimitiveType的数组
  • ArrayType NewArray(JNIEnv *env, jsize length); 指定一个长度然后返回相应的Java基本类型的数组
方法返回值
NewArray RoutinesArray Type
NewBooleanArray()jbooleanArray
NewByteArray()jbyteArray
NewCharArray()jcharArray
NewShortArray()jshortArray
NewIntArray()jintArray
NewLongArray()jlongArray
NewFloatArray()jfloatArray
NewDoubleArray()jdoubleArray
2.7.4 创建类型为elementClass的数组
  • jobjectArray NewObjectArray(JNIEnv *env, jsize length, jclass elementClass, jobject initialElement);

造一个新的数据组,类型是elementClass,所有类型都被初始化为initialElement。

2.7.5 获取数组中某个位置的元素
  • jobject GetObjectArrayElement(JNIEnv *env, jobjectArray array, jsize index);

返回Object数组的一个元素

2.7.6 获取数组的长度
  • jsize GetArrayLength(JNIEnv *env, jarray array);

获取array数组的长度.

3. JNI的引用

Java内存管理这块是完全透明的,new一个实例时,只知道创建这个类的实例后,会返回这个实例的一个引用,然后拿着这个引用去访问它的成员(属性、方法),完全不用管JVM内部是怎么实现的,如何为新建的对象申请内存,使用完之后如何释放内存,只需要知道有个垃圾回收器在处理这些事情就行了,然而,从Java虚拟机创建的对象传到C/C++代码就会产生引用,根据Java的垃圾回收机制,只要有引用存在就不会触发该该引用所指向Java对象的垃圾回收。

在JNI规范中定义了三种引用:

  • 局部引用(Local Reference)
  • 全局引用(Global Reference)
  • 弱全局引用(Weak Global Reference)。

上面三种JNI引用的作用不同,作用域也不尽一样,也有着不同的生命周期:

  • 对于局部引用,在本地方法被调用时创建,在方法返回时(return),该引用将会自动被释放,因此在方法返回之后再使用该引用是不合法的。并不是任意引用都可以使用在所有上下文环境。但在其有效时,将一直阻止所引用的对象被GC回收。

  • 对于全局引用和弱全局引用,它们可以在多个方法中使用,在手动释放之前一直有效。但是弱全局引用不会阻止GC回收它所引用的对象,因此使用此引用前判空是必要的env->IsSameObject(obj1, obj2)

3.1 局部引用(Local Reference)
  • 绝大多数JNI 方法返回的引用是局部引用,它保证了在引用有效的情况下,所引用的对象不会被GC回收。同时也说明,当局部引用不再使用时,仍然引用了对象而不得回收,造成内存消耗。
  • 局部引用的作用域或生命周期始于创建它的本地方法,终于本地方法返回。因此,不要尝试使用静态变量来缓存局部引用以达到复用,因为缓存的引用将在本地方法返回时变的无效,因而下次继续使用缓存的引用来操作时,将会触发内存崩溃。
  • 通常在局部引用不再使用时,可以显示使用DeleteLocalRef来提前释放它所指向的对象,以便于GC回收。显示的删除局部引用,是为了让它所指的对象能够及时被GC回收,以便节省内存空间,否则只有等到本地方法返回。
  • 在一个局部引用被销毁前,可能经过了数个本地方法的传递,最终会在某个方法里决定销毁它或最终被自动销毁。因此在不需要再使用该局部引用时最好释放它,因为它所引用的可能是大对象,占用较多的内存,长此以往可能造成内存吃紧。另外,在所传递的本地方法中创建了局部引用而不释放它,会造成局部引用的数量积累,严重情况可能导致JNI局部引用表的溢出,导致程序崩溃。
  • 局部引用是线程相关的,只能在创建它的线程里使用,通过全局变量缓存并使用在其他线程是不合法的。
extern "C" JNIEXPORT jstring JNICALL
Java_com_gavinchoo_jni_learning_LearningJni_newStringNative(JNIEnv *env, jobject /* this */) {
    int strLen = strlen("java/lang/String");
    jclass jStringClass = env->FindClass("java/lang/String");
    // 获取构造方法的method id ,参数为byte[]
    jmethodID methodId = env->GetMethodID(jStringClass, "<init>", "([BLjava/lang/String;)V");

    // 创建一个字节数组
    jbyteArray byteArray = env->NewByteArray(strLen);
    jstring encode = env->NewStringUTF("utf-8");
    // 填充数据
    env->SetByteArrayRegion(byteArray, 0, strLen, (jbyte *) "java/lang/String");
    // 创建并返回String
    jstring str = (jstring) env->NewObject(jStringClass, methodId, byteArray, encode);
    // 释放对象
    env->DeleteLocalRef(jStringClass);
    return str;
}
3.2 全局引用(Global Reference)
  • 通过NewGlobalRef方法来创建全局引用,通过DeleteGlobalRef来释放全局引用。
  • 全局引用一直保持有效,直到被程序员手动释放。
  • 在失效之前,全局引用确保了所引用的对象不会被GC回收。全局引用可以使用在多次方法调用中,也可以跨方法使用,在多个线程中使用也是合法有效的。
  • 可以使用静态变量来缓存全局引用,可以达到复用目的,节省相关性能开销。需要注意的是,全局引用会一直阻止对象被回收,因此合理的释放全局引用是必要的。
// 缓存string class
static jclass jStringClass = NULL;

extern "C" JNIEXPORT jstring JNICALL
Java_com_gavinchoo_jni_learning_LearningJni_newGlobalStringNative(JNIEnv *env,
                                                                   jobject /* this */) {

    if (jStringClass == NULL) {
        jclass jlocalClass = env->FindClass("java/lang/String");
        if (jlocalClass == NULL)
            return NULL;
        jStringClass = static_cast<jclass>(env->NewGlobalRef(jlocalClass));  // 创建全局引用
        env->DeleteLocalRef(jlocalClass); // 删除局部引用
        LOGE("jStringClass is null.just create it");
    } else {
        LOGE("jStringClass is non-null");
    }

    int strLen = strlen("java/lang/String");

    jmethodID methodId = env->GetMethodID(jStringClass, "<init>", "([BLjava/lang/String;)V");
    // 创建一个字节数组
    jbyteArray byteArray = env->NewByteArray(strLen);
    jstring encode = env->NewStringUTF("utf-8");
    // 填充数据
    env->SetByteArrayRegion(byteArray, 0, strLen, (jbyte *) "java/lang/String");
    // 创建并返回String
    jstring str = (jstring) env->NewObject(jStringClass, methodId, byteArray, encode);
    // 释放全局引用,如果不释放,日志打印"jStringClass is non-null"
    env->DeleteGlobalRef(jStringClass);
    jStringClass = NULL;
    return str;
}
3.3 弱全局引用(Weak Global Reference)
  • 使用NewWeakGlobalRef来创建,使用DeleteGlobalWeakRef来释放,使引用无效。
  • 类似全局引用,在无效之前,可以跨方法多次使用,也可以在多线程中使用,无效时使用会导致异常。
  • 在引用有效的情况下,不保证所引用的对象不被GC回收,因此在使用该引用时,需要做判空处理,使用IsSameObject来检查是否存在。
3.4 引用比较

在给定两个引用,不管是什么引用,我们只需要调用IsSameObject函数来判断他们是否是指向相同的对象。代码如下:

env->IsSameObject(obj1, obj2)

如果obj1和obj2指向相同的对象,则返回JNI_TRUE(或者1),否则返回JNI_FALSE(或者0)

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值