JNI相关使用记录
JNI 工作流程
- java层调用system.load方法。
- 通过classloader拿到了so文件的绝对路径,然后调用nativeload()方法。
- 通过linux下的dlopen方法,加载并查找so库里的方法。
- 当前线程下的 JNIENV 会将所有的jni方法注册到了同一个Jvm中,so和class到了同一个进程空间
(人脸项目中就是在Strom的一个Worker JVM,多个executor线程共享一个faceEengine对象)(JNIENV 代表了java在本线程的运行环境,每个线程都有一个)。 - 通过当前线程的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) ) )
- Heap Memory:供Java应用程序使用,所有java对象的内存都是从这里分配的,它物理上不连续,但是逻辑上是连续的。可通过java命令行参数“-Xms, -Xmx”大设置Heap初始值和最大值。
- 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!");
- jstring类型是JNI提供的,对应于Java的String类型
- JNI函数NewStringUTF()用于构造一个String对象,该对象存放在Java Heap中,同时返回了一个jstring类型的引用。
- 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。
-
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的区别在于生命周期和作用范围:
- Global Reference是通过JNI函数NewGlobalRef()和DeleteGlobalRef()来创建和删除的。
- 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负载,提高运行速度。- jmethodID/jfielID 和 jobject 没有继承关系,它不是个object,只是个整数,不存在被释放与否的问题,可用全局变量保存。
- jclass、jstring是由jobject继承而来的类,所以它是个jobject,需要用全局变量保存。
-
局部引用管理new出来的对象,注意及时delete。总体原则,注意释放所有对jobject的引用。
-
不同线程使用JNIEnv*对象,需要AttachCurrentThread将env挂到当前线程,否则无法使用env。
-
尽量避免频繁调用JNI或者是使用JNI传输大量到数据。
开发相关
二维数组
二维数组具有特殊性在于,可以将它看成一维数组,其中数组的每项内容又是一维数组。
参考
- 基础知识:
- 开发相关: