在计算机系统中,每一种编程语言都有一个执行环境(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 Routines | Array 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)。