JNI是什么:JNI是Java Native Interface的缩写,提供了若干API实现了Java和其他语言的通信(主要是C&C++)。
为什么要用JNI:在C/C++中写的程序可以避开JVM的内存开销过大的限制、处理高性能的计算、调用系统服务(例如驱动)等功能。
JVM: jvm是java虚拟机在jni层的代表,全局只有一个。
JNIENV: 代表了java在本线程的运行环境,每个线程都有一个。
JOBJECT: 在JNI中除了基本类型数组、Class、String和Throwable外其余所有Java对象的数据类型在JNI中都用jobject表示
如图:
1. java层调用system.load方法。
2. 通过classloader拿到了so的绝对路径,然后调用nativeload()方法。
3. 通过linux下的dlopen方法,加载并查找so库里的方法,如有jni_onload会优先加载。
4. 当前线程下的jnienv会将所有的jni方法注册到了同一个vm中,so和class到了同一个进程空间。
5. 通过当前线程的jnienv即可调用对应的对象方法了。
先看一张native方法和java方法的调用同样方法的耗时统计图,可以看到,2者耗时差距为11倍。
设备 |
native首次 |
native 1万次 |
java首次 |
java一万次 |
荣耀3X |
0.04 |
10.56 |
0.02 |
1.17 |
NOTE3 |
0.03 |
3.02 |
0.009 |
0.04 |
NOTE4 |
0.07 |
3.56 |
0.06 |
0.19 |
NEXUS5 |
0.07 |
3.44 |
0.02 |
0.35 |
NEXUS6 |
0.20 |
4.12 |
0.03 |
0.40 |
平均 |
0.082 |
4.96 |
0.027 |
0.43 |
倍数 |
3.0 |
11.5 |
原因:
C++调用java需要查找类,查找方法,查找方法ID,获取字段或者方法的调用有时候会需要在JVM中完成大量工作,因为字段和方法可能是从超类中继承而来的,为特定类返回的id不会在Jvm进程生存期间发生变化 ,这会让jvm向上遍历类层次结构来找到它们,这是个开销很大的操作。
所以,缓存ID字段是为了降低CPU负载,提高运行速度,节约电量。
Global Reference: 全局引用生存周期为创建后,直到程序员显示的释放它,否则一直存在。
全局引用可以在多线程之间共享其指向的对象。
Local Reference : 局部引用生存周期为创建后,直到DeleteLocalRef . 或在该方法结束后没有被JVM发现有JAVA层引用而被JVM回收并释放。
局部引用只对当前线程有效,多个线程间不能共享局部引用。
注意:
基于谁创建谁销毁的原则,native函数执行完后,局部引用没有被native代码显示删除,那么局部引用在JVM中还是有效的,JVM决定什么时候删除它,和C语言的局部变量含义是不一样的。
局部引用在JVM中是有个数限制的,默认16个,注意管理释放。
Weak Global Reference :
弱全局引用生命周期为创建之后,直到DeleteGlobalRef。或在内存紧张时进行回收而被释放。
注意:
jobject默认是local Ref,函数环境消失时会跟随消失
在jni_onload初始化全局引用和弱全局引用
jmethodID/jfielID和jobject没有继承关系,他不是个object,只是个整数,不存在被释放与否的问题,可用全局变量保存。
jclass是由jobject继承而来的类,所以它是个jobject,需要用弱全局引用来缓存jclass对象。
局部引用管理new出来的对象,注意及时delete。
总体原则,注意释放所有对jobject的引用。
缓存与不缓存的效果对比:调用速度提高40倍
设备 |
缓存 |
不缓存 |
NOTE3 |
12.06 |
420.82 |
NOTE4 |
7.96 |
495 |
NOTE5 |
4.3 |
277.42 |
NEXUS6P |
7.28 |
83.76 |
平均 |
7.9 |
319.25 |
倍数 |
40.4 |
TIPS:
1. 不同线程使用JNIEnv*对象,需要AttachCurrentThread将env挂到当前线程,否则无法使用env。
2. 尽量避免频繁调用JNI或者是使用JNI传输大量到数据。