衆所周知,Java的JNI調用會有很昂貴的固有開銷,這主要是出自一系列的安全檢查,在Java設計之初,開發者但願將JVM打形成一個與外界互相隔離的銅牆鐵壁(別忘天國的Java Applet),所以在JNI調用中會有大量的檢查,棧爆上的這篇回覆解釋了JNI爲什麼"如此之慢"的緣由,在正式調用一個C/C++函數以前和以後共須要15步額外的工做! 並且這還不包括額外的數據校驗工做,在經過JNI向外傳出對象或數組時Java會進行額外的檢查,這又更進一步下降了性能. 所以在性能關鍵的情境中JNI不多被使用,這些年間曾涌現過一些解決JNI開銷的方法,好比CriticalArray或NIO的DirectBuffer等,但效果都頗有限. java
事實上龜殼對JNI效率低的問題也很捉急,所以在Java7時代龜殼在Hotspot中推出過一個未公開的功能: CriticalNative. 說到CriticalNative,首先要先解釋下它的老前輩CriticalArray,在JNI中,"正規"的操做數組的方式是使用GetXXXArrayElements和ReleaseXXXArrayElements,然而用過的人都會知道它是慢的有多吃屎,這是由於開發者考慮到在數組操做時可能發生GC致使數組在內存中的位置發生變化,以及直接將Java堆上的內存地址交給用戶有些不安全,所以GetXXXArrayElements返回給用戶的是一個數組副本,而ReleaseXXXArrayElements則是將副本複製回Java堆中真實的數組裏. 而CriticalArray則是爲了解決數組副本問題,它是經過在GetPrimitiveArrayCritical和ReleasePrimitiveArrayCritical中建立一個阻止GC的臨界區,得以將數組的真實數據直接暴露給用戶. 算法
而CriticalNative則是在此之上更進一步,它是一種特殊的JNI函數,整個函數都是一個臨界區(固然,也包括跳過一些非關鍵的安全檢查),可以以犧牲JVM總體穩定性獲取最大的性能. 因爲最初是被設計爲JRE的加密模塊使用,考慮到如今的加密算法大多以塊爲單位,換句話說大多數狀況下須要在JNI中頻繁傳遞小規模的數組,CriticalNative被專門設計對數組的傳遞進行優化. 數組
想讓一個JNI函數成爲CriticalNative,須要以下修改/條件: 安全
JRE7或更高版本下的Hotspot虛擬機 jvm
JNI函數的前綴由"Java_"改成"JavaCritical_" 函數
必須是static方法,不能有synchronized 性能
參數中不能有對象、對象數組或多維數組 優化
原先的普通版本("Java_"開頭的)不能去掉 加密
在成爲CriticalNative後,函數有以下特性. spa
參數中沒有了JNIEnv*和jclass/jobject,基本類型參數不變,數組分紅2個參數,頭一個參數爲jint,表明數組長度,後一個參數爲jXXX*,即相應類型的指針
因爲沒有了JNIEnv,顯然函數中沒法調用任何Java的東西
整個函數成爲臨界區,會阻礙垃圾回收的進行
此外,CriticalNative還有個奇(keng)特(die)的特性,就是懶加載,在最初的必定次數調用中,JVM始終調用的是正常版本,只有達到必定閾值後,纔會開始調用CriticalNative版本,這個特性當初也把我坑過幾回.這個閾值和時間無關,只與調用次數有關.
舉個栗子,以一個在Java中經過SSE計算兩個4x4矩陣乘法的native方法爲例,若是那個方法名字叫mul,位於mypackage包中的MyClass類中,沒有其餘形式的重載,參數是兩個float[],那麼它的標準JNI函數應該是.
JNIEXPORT void JNICALL Java_mypackage_MyClass_mul( JNIEnv *env, jclass klass, jfloatArray mat1, jfloatArray mat2) { float *A = static_cast<float*>(env->GetPrimitiveArrayCritical(mat1, 0)); float *B = static_cast<float*>(env->GetPrimitiveArrayCritical(mat2, 0)); sseMul(A, B); //計算乘法 env->ReleasePrimitiveArrayCritical(mat1, A, 0); env->ReleasePrimitiveArrayCritical(mat2, B, JNI_ABORT); }
若是要添加它的CriticalNative形式,那就在保留原函數的同時,再額外加一個:
JNIEXPORT void JNICALL JavaCritical_mypackage_MyClass_mul( jint length1, jfloat* mat1, jint length2, jfloat* mat2) { sseMul(mat1, mat2); }
而後就無需任何額外的工做了,在Hotspot虛擬機中,最初調用mul函數時會依舊調用標準版本,當總調用次數達到必定次數時,就會開始調用Critical版本,在個人機器上,標準版本的耗時約爲61.2ns,Critical版本的耗時約爲14.6ns,其中SSE運算佔6ns,做爲對比,純Java實現的4x4矩陣乘法耗時約爲25.6ns.
做爲缺點,除了在CriticalNative中沒法使用JNIEnv和對象與字符串參數、在調用時系統沒法進行GC之外,CriticalNative最大的缺點恐怕是隻能在Java7以及更高版本的Hotspot中工做,不過龜殼也在努力改變這個問題,Project Panama即是要大幅優化JNI調用,聽說它最新的原型已經能夠在JIT中生成JNI函數的直接調用了,不過顯然它趕不上今年5月的Java9特性凍結,甚至可否加入Java10都是一個未知數,無論怎麼樣,祝他們好運.
參考: