引子
Android
中有@CriticalNative
注解:
https://source.android.google.cn/devices/tech/dalvik/improvements
里面说到:
@FastNative 可以使原生方法的性能提升高达 2 倍,@CriticalNative 则可以提升高达4倍。
那么这是怎么做到的呢?
native方法
调用native方法时,JVM的工作步骤:
(源码: http://hg.openjdk.java.net/jdk8u/jdk8u/hotspot/file/4d9931ebf861/src/cpu/x86/vm/sharedRuntime_x86_64.cpp#l1723)
- 创建栈帧;
- 根据ABI移动参数到寄存器或者栈;(ABI: 应用二进制接口)
- 封装对象引用到JNI handlers;
- 获取静态方法的
JNIEnv*
和jclass
,把他们作为额外参数传递;- 检查是否调用
method_entry
的trace函数;- 检查是否调用对象锁;(
synchronized
)(optinal)- 检查native方法是否已经链接;(懒加载函数检查、链接)
- 线程状态从
in_java
转变为in_native
;- 调用native方法;
- 检查是否需要safepoint;
- 线程状态转回
in_java
;- 解锁对象锁;(optional)
- notify
method_exit
;(optional)- 将对象结果解出,重置JNI handlers;
- 处理JNI异常;
- 移除栈帧。
开销比较大,主要是用于各种参数拷贝,尤其是遇到数组,需要来回拷贝、检查。
此时,如果是足够简单的native方法,可以用Critical Natives
来降低开销。
Critical Native方法
Critical Natives
方法是需要满足下列约束的native
方法:
- 必须是static且没有synchronized; (省掉上一节的6、12步)
- 参数类型必须是基本类型或基本类型的数组;(省掉上一节中的对象相关3、14)
- 具体实现不能调用JNI函数(也就是不使用
JNIEnv* env
和jclass cls
,既然不使用就不用传给它了),不能分配java对象或者抛出异常;(省掉上一节中的4、15)- 不能运行太长时间.(因为它会阻塞gc)
基于这个原理的话, critical native
方法比普通native
方法快的原因其实是节省了一些调用开始和结束的开销,因此如果被调用的方法如果是时间占用的大头的话,其实这个优化幅度就很小了。
反之如果是频繁调用的方法,而且每次调用的数据量很小,此时调用开销和执行开销是同量级,那么累计的优化幅度就会很大。
(比如只是长度为16的数组计算的话,计算力提升可以达到2~3倍。)
满足上述约束以后,Critical Natives
方法还需要进行下列声明:
- 方法名以
JavaCritical_
开头;- 没有额外的
JNIEnv*
和jclass
参数;(因为是static方法,自然也就没有jobject参数了)- java数组传递的时候用两个参数: 数组长度、数组引用(基本类型)。
// 这样不再需要调用GetArrayLength
、GetByteArrayElements
等函数。
此外critical natives方法变成临界区。native
方法示例:
1 | JNIEXPORT jint JNICALL |
Critical Natives
方法示例:
1 | JNIEXPORT jint JNICALL |
critical
版本的方法是JIT需要的(默认是调用超过1500次,可以调JIT参数-XX:CompileThreshold=invocations
);
普通native
版本的方法是解释器需要的;
因此实际用的时候,这俩版本的代码都要写上。比如是这样的:
1 |
|
(之所以这么繁琐的原因是这个特性和Unsafe一样是jdk内部使用的,没有公开发布给普通程序员,正式发布估计要到jdk10了)
参考:
http://cr.openjdk.java.net/~jrose/panama/native-call-primitive.html
http://mail.openjdk.java.net/pipermail/panama-dev/2015-December/000225.html
https://stackoverflow.com/questions/36298111/is-it-possible-to-use-sun-misc-unsafe-to-call-c-functions-without-jni/36309652#36309652