Java JNI深度分析與實踐
Java 環(huán)境和語言對于應用程序開發(fā)來說是非常安全和高效的。但是,一些應用程序卻需要執(zhí)行純 Java 程序無法完成的一些任務,比如:
與舊有代碼集成,避免重新編寫。
實現(xiàn)可用類庫中所缺少的功能。舉例來說,在 Java 語言中實現(xiàn) ping 時,您可能需要 Internet Control Message Protocol (ICMP) 功能,但基本類庫并未提供它。
***與使用 C/C++ 編寫的代碼集成,以充分發(fā)掘性能或其他與環(huán)境相關的系統(tǒng)特性。
解決需要非 Java 代碼的特殊情況。舉例來說,核心類庫的實現(xiàn)可能需要跨包調(diào)用或者需要繞過其他 Java 安全性檢查。
JNI 允許您完成這些任務。它明確分開了 Java 代碼與本機代碼(C/C++)的執(zhí)行,定義了一個清晰的 API 在這兩者之間進行通信。從很大程度上說,它避免了本機代碼對 JVM 的直接內(nèi)存引用,從而確保本機代碼只需編寫一次,并且可以跨不同的 JVM 實現(xiàn)或版本運行。
借助 JNI,本機代碼可以隨意與 Java 對象交互,獲取和設計字段值,以及調(diào)用方法,而不會像 Java 代碼中的相同功能那樣受到諸多限制。這種自由是一把雙刃劍:它犧牲 Java 代碼的安全性,換取了完成上述所列任務的能力。在您的應用程序中使用 JNI 提供了強大的、對機器資源(內(nèi)存、I/O 等)的低級訪問,因此您不會像普通 Java 開發(fā)人員那樣受到安全網(wǎng)的保護。JNI 的靈活性和強大性帶來了一些編程實踐上的風險,比如導致性能較差、出現(xiàn) bug 甚至程序崩潰。您必須格外留意應用程序中的代碼,并使用良好的實踐來保障應用程序的總體完整性。
本文介紹 JNI 用戶最常遇到的 10 大編碼和設計錯誤。其目標是幫助您認識到并避免它們,以便您可以編寫安全、高效、性能出眾的 JNI 代碼。本文還將介紹一些用于在新代碼或已有代碼中查找這些問題的工具和技巧,并展示如何有效地應用它們。
Java JNI 編程缺陷可以分為兩類:
性能:代碼能執(zhí)行所設計的功能,但運行緩慢或者以某種形式拖慢整個程序。
正確性:代碼有時能正常運行,但不能可靠地提供所需的功能;最壞的情況是造成程序崩潰或掛起。
性能缺陷
程序員在使用 JNI 時的 5 大性能缺陷如下:
◆不緩存方法 ID、字段 ID 和類
◆觸發(fā)數(shù)組副本
◆回訪(Reaching back)而不是傳遞參數(shù)
◆錯誤認定本機代碼與 Java 代碼之間的界限
◆使用大量本地引用,而未通知 JVM
◆不緩存方法 ID、字段 ID 和類
要訪問 Java 對象的字段并調(diào)用它們的方法,本機代碼必須調(diào)用 FindClass()、GetFieldID()、GetMethodId() 和 GetStaticMethodID()。對于 GetFieldID()、GetMethodID() 和 GetStaticMethodID(),為特定類返回的 ID 不會在 JVM 進程的生存期內(nèi)發(fā)生變化。但是,獲取字段或方法的調(diào)用有時會需要在 JVM 中完成大量工作,因為字段和方法可能是從超類中繼承而來的,這會讓 JVM 向上遍歷類層次結(jié)構(gòu)來找到它們。由于 ID 對于特定類是相同的,因此您只需要查找一次,然后便可重復使用。同樣,查找類對象的開銷也很大,因此也應該緩存它們。
舉例來說,清單 1 展示了調(diào)用靜態(tài)方法所需的 JNI 代碼:
清單 1. 使用 JNI 調(diào)用靜態(tài)方法
- int val=1;
- jmethodID method;
- jclass cls;
- cls = (*env)->FindClass(env, "com/ibm/example/TestClass");
- if ((*env)->ExceptionCheck(env)) {
- return ERR_FIND_CLASS_FAILED;
- }
- method = (*env)->GetStaticMethodID(env, cls, "setInfo", "(I)V");
- if ((*env)->ExceptionCheck(env)) {
- return ERR_GET_STATIC_METHOD_FAILED;
- }
- (*env)->CallStaticVoidMethod(env, cls, method,val);
- if ((*env)->ExceptionCheck(env)) {
- return ERR_CALL_STATIC_METHOD_FAILED;
- }
當我們每次希望調(diào)用方法時查找類和方法 ID 都會產(chǎn)生六個本機調(diào)用,而不是***次緩存類和方法 ID 時需要的兩個調(diào)用。
緩存會對您應用程序的運行時造成顯著的影響??紤]下面兩個版本的方法,它們的作用是相同的。清單 2 使用了緩存的字段 ID:
清單 2. 使用緩存的字段 ID
- int sumValues2(JNIEnv* env, jobject obj, jobject allValues){
- jint avalue = (*env)->GetIntField(env, allValues, a);
- jint bvalue = (*env)->GetIntField(env, allValues, b);
- jint cvalue = (*env)->GetIntField(env, allValues, c);
- jint dvalue = (*env)->GetIntField(env, allValues, d);
- jint evalue = (*env)->GetIntField(env, allValues, e);
- jint fvalue = (*env)->GetIntField(env, allValues, f);
- return avalue + bvalue + cvalue + dvalue + evalue + fvalue;
- }
清單 3. 未緩存字段 ID
- int sumValues2(JNIEnv* env, jobject obj, jobject allValues){
- jclass cls = (*env)->GetObjectClass(env,allValues);
- jfieldID a = (*env)->GetFieldID(env, cls, "a", "I");
- jfieldID b = (*env)->GetFieldID(env, cls, "b", "I");
- jfieldID c = (*env)->GetFieldID(env, cls, "c", "I");
- jfieldID d = (*env)->GetFieldID(env, cls, "d", "I");
- jfieldID e = (*env)->GetFieldID(env, cls, "e", "I");
- jfieldID f = (*env)->GetFieldID(env, cls, "f", "I");
- jint avalue = (*env)->GetIntField(env, allValues, a);
- jint bvalue = (*env)->GetIntField(env, allValues, b);
- jint cvalue = (*env)->GetIntField(env, allValues, c);
- jint dvalue = (*env)->GetIntField(env, allValues, d);
- jint evalue = (*env)->GetIntField(env, allValues, e);
- jint fvalue = (*env)->GetIntField(env, allValues, f);
- return avalue + bvalue + cvalue + dvalue + evalue + fvalue
清單 2 用 3,572 ms 運行了 10,000,000 次。清單 3 用了 86,217 ms — 多花了 24 倍的時間。
觸發(fā)數(shù)組副本
JNI 在 Java 代碼和本機代碼之間提供了一個干凈的接口。為了維持這種分離,數(shù)組將作為不透明的句柄傳遞,并且本機代碼必須回調(diào) JVM 以便使用 set 和 get 調(diào)用操作數(shù)組元素。Java 規(guī)范讓 JVM 實現(xiàn)決定讓這些調(diào)用提供對數(shù)組的直接訪問,還是返回一個數(shù)組副本。舉例來說,當數(shù)組經(jīng)過優(yōu)化而不需要連續(xù)存儲時,JVM 可以返回一個副本。(參見 參考資料 獲取關于 JVM 的信息)。
隨后,這些調(diào)用可以復制被操作的元素。舉例來說,如果您對含有 1,000 個元素的數(shù)組調(diào)用 GetLongArrayElements(),則會造成至少分配或復制 8,000 字節(jié)的數(shù)據(jù)(每個 long 1,000 元素 * 8 字節(jié))。當您隨后使用 ReleaseLongArrayElements() 更新數(shù)組的內(nèi)容時,需要另外復制 8,000 字節(jié)的數(shù)據(jù)來更新數(shù)組。即使您使用較新的 GetPrimitiveArrayCritical(),規(guī)范仍然準許 JVM 創(chuàng)建完整數(shù)組的副本。
GetTypeArrayRegion() 和 SetTypeArrayRegion() 方法允許您獲取和更新數(shù)組的一部分,而不是整個數(shù)組。通過使用這些方法訪問較大的數(shù)組,您可以確保只復制本機代碼將要實際使用的數(shù)組部分。
舉例來說,考慮相同方法的兩個版本,如清單 4 所示:
清單 4. 相同方法的兩個版本
- jlong getElement(JNIEnv* env, jobject obj, jlongArray arr_j,
- int element){
- jboolean isCopy;
- jlong result;
- jlong* buffer_j = (*env)->GetLongArrayElements(env, arr_j, &isCopy);
- result = buffer_j[element];
- (*env)->ReleaseLongArrayElements(env, arr_j, buffer_j, 0);
- return result;
- }
- jlong getElement2(JNIEnv* env, jobject obj, jlongArray arr_j,
- int element){
- jlong result;
- (*env)->GetLongArrayRegion(env, arr_j, element,1, &result);
- return result;
- }
- }
【編輯推薦】