久久国产成人av_抖音国产毛片_a片网站免费观看_A片无码播放手机在线观看,色五月在线观看,亚洲精品m在线观看,女人自慰的免费网址,悠悠在线观看精品视频,一级日本片免费的,亚洲精品久,国产精品成人久久久久久久

分享

使用JNI的最佳實踐

 armok_hhza 2010-09-10

Java? 本機接口(Java Native Interface,,JNI)是一個標準的 Java API,,它支持將 Java 代碼與使用其他編程語言編寫的代碼相集成。如果您希望利用已有的代碼資源,,那么可以使用 JNI 作為您工具包中的關(guān)鍵組件 —— 比如在面向服務(wù)架構(gòu)(SOA)和基于云的系統(tǒng)中,。但是,如果在使用時未注意某些事項,,則 JNI 會迅速導致應(yīng)用程序性能低下且不穩(wěn)定,。本文將確定 10 大 JNI 編程缺陷,提供避免這些缺陷的最佳實踐,,并介紹可用于實現(xiàn)這些實踐的工具,。

Java 環(huán)境和語言對于應(yīng)用程序開發(fā)來說是非常安全和高效的。但是,,一些應(yīng)用程序卻需要執(zhí)行純 Java 程序無法完成的一些任務(wù),,比如:

  • 與舊有代碼集成,避免重新編寫,。

  • 實現(xiàn)可用類庫中所缺少的功能,。舉例來說,在 Java 語言中實現(xiàn) ping 時,,您可能需要 Internet Control Message Protocol (ICMP) 功能,,但基本類庫并未提供它。

  • 最好與使用 C/C++ 編寫的代碼集成,,以充分發(fā)掘性能或其他與環(huán)境相關(guān)的系統(tǒng)特性,。

  • 解決需要非 Java 代碼的特殊情況。舉例來說,,核心類庫的實現(xiàn)可能需要跨包調(diào)用或者需要繞過其他 Java 安全性檢查,。

JNI 允許您完成這些任務(wù)。它明確分開了 Java 代碼與本機代碼(C/C++)的執(zhí)行,,定義了一個清晰的 API 在這兩者之間進行通信,。從很大程度上說,,它避免了本機代碼對 JVM 的直接內(nèi)存引用,從而確保本機代碼只需編寫一次,,并且可以跨不同的 JVM 實現(xiàn)或版本運行,。

借助 JNI,本機代碼可以隨意與 Java 對象交互,,獲取和設(shè)計字段值,,以及調(diào)用方法,而不會像 Java 代碼中的相同功能那樣受到諸多限制,。這種自由是一把雙刃劍:它犧牲 Java 代碼的安全性,換取了完成上述所列任務(wù)的能力,。在您的應(yīng)用程序中使用 JNI 提供了強大的,、對機器資源(內(nèi)存、I/O 等)的低級訪問,,因此您不會像普通 Java 開發(fā)人員那樣受到安全網(wǎng)的保護,。JNI 的靈活性和強大性帶來了一些編程實踐上的風險,比如導致性能較差,、出現(xiàn) bug 甚至程序崩潰,。您必須格外留意應(yīng)用程序中的代碼,并使用良好的實踐來保障應(yīng)用程序的總體完整性,。

本文介紹 JNI 用戶最常遇到的 10 大編碼和設(shè)計錯誤,。其目標是幫助您認識到并避免它們,以便您可以編寫安全,、高效,、性能出眾的 JNI 代碼。本文還將介紹一些用于在新代碼或已有代碼中查找這些問題的工具和技巧,,并展示如何有效地應(yīng)用它們,。

JNI 編程缺陷可以分為兩類:

  • 性能:代碼能執(zhí)行所設(shè)計的功能,但運行緩慢或者以某種形式拖慢整個程序,。
  • 正確性:代碼有時能正常運行,,但不能可靠地提供所需的功能;最壞的情況是造成程序崩潰或掛起,。

性能缺陷

程序員在使用 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 對于特定類是相同的,因此您只需要查找一次,,然后便可重復使用,。同樣,查找類對象的開銷也很大,,因此也應(yīng)該緩存它們,。

舉例來說,清單 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)用,。

緩存會對您應(yīng)用程序的運行時造成顯著的影響??紤]下面兩個版本的方法,,它們的作用是相同的。清單 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;
}

 

性能技巧 #1

查找并全局緩存常用的類,、字段 ID 和方法 ID,。

清單 3 沒有使用緩存的字段 ID:


清單 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 可以返回一個副本,。(參見 參考資料 獲取關(guān)于 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ù)組的副本,。

性能技巧 #2

獲取和更新僅本機代碼需要的數(shù)組部分,。在只要數(shù)組的一部分時通過適當?shù)?API 調(diào)用來避免復制整個數(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;
}

 

第一個版本可以生成兩個完整的數(shù)組副本,,而第二個版本則完全沒有復制數(shù)組,。當數(shù)組大小為 1,000 字節(jié)時,運行第一個方法 10,000,000 次用了 12,055 ms,;而第二個版本僅用了 1,421 ms。第一個版本多花了 8.5 倍的時間,!

性能技巧 #3

在單個 API 調(diào)用中盡可能多地獲取或更新數(shù)組內(nèi)容,。如果可以一次較多地獲取和更新數(shù)組內(nèi)容,則不要逐個迭代數(shù)組中的元素,。

另一方面,,如果您最終要獲取數(shù)組中的所有元素,則使用 GetTypeArrayRegion() 逐個獲取數(shù)組中的元素是得不償失的,。要獲取最佳的性能,,應(yīng)該確保以盡可能大的塊的來獲取和更新數(shù)組元素。如果您要迭代一個數(shù)組中的所有元素,,則 清單 4 中這兩個 getElement() 方法都不適用,。比較好的方法是在一個調(diào)用中獲取大小合理的數(shù)組部分,然后再迭代所有這些元素,,重復操作直到覆蓋整個數(shù)組,。

回訪而不是傳遞參數(shù)

在調(diào)用某個方法時,您經(jīng)常會在傳遞一個有多個字段的對象以及單獨傳遞字段之間做出選擇,。在面向?qū)ο笤O(shè)計中,,傳遞對象通常能提供較好的封裝,因為對象字段的變化不需要改變方法簽名,。但是,,對于 JNI 來說,本機代碼必須通過一個或多個 JNI 調(diào)用返回到 JVM 以獲取需要的各個字段的值,。這些額外的調(diào)用會帶來額外的開銷,,因為從本機代碼過渡到 Java 代碼要比普通方法調(diào)用開銷更大,。因此,對于 JNI 來說,,本機代碼從傳遞進來的對象中訪問大量單獨字段時會導致性能降低,。

考慮清單 5 中的兩個方法,第二個方法假定我們緩存了字段 ID:


清單 5. 兩個方法版本

				
int sumValues(JNIEnv* env, jobject obj, jint a, jint b,jint c, jint d, jint e, jint f){
return a + b + c + d + e + f;
}

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;
}

 

性能技巧 #4

如果可能,,將各參數(shù)傳遞給 JNI 本機代碼,,以便本機代碼回調(diào) JVM 獲取所需的數(shù)據(jù)。

sumValues2() 方法需要 6 個 JNI 回調(diào),,并且運行 10,000,000 次需要 3,572 ms,。其速度比 sumValues() 慢 6 倍,后者只需要 596 ms,。通過傳遞 JNI 方法所需的數(shù)據(jù),,sumValues() 避免了大量的 JNI 開銷。

錯誤認定本機代碼與 Java 代碼之間的界限

本 機代碼和 Java 代碼之間的界限是由開發(fā)人員定義的,。界限的選定會對應(yīng)用程序的總體性能造成顯著的影響,。從 Java 代碼中調(diào)用本機代碼以及從本機代碼調(diào)用 Java 代碼的開銷比普通的 Java 方法調(diào)用高很多。此外,,這種越界操作會干擾 JVM 優(yōu)化代碼執(zhí)行的能力,。舉例來說,隨著 Java 代碼與本機代碼之間互操作的增加,,實時編譯器的效率會隨之降低,。經(jīng)過測量,我們發(fā)現(xiàn)從 Java 代碼調(diào)用本機代碼要比普通調(diào)用多花 5 倍的時間,。同樣,,從本機代碼中調(diào)用 Java 代碼也需要耗費大量的時間。

性能技巧 #5

定義 Java 代碼與本機代碼之間的界限,,最大限度地減少兩者之間的互相調(diào)用,。

因 此,在設(shè)計 Java 代碼與本機代碼之間的界限時應(yīng)該最大限度地減少兩者之間的相互調(diào)用,。消除不必要的越界調(diào)用,,并且應(yīng)該竭力在本機代碼中彌補越界調(diào)用造成的成本損失。最大限度地減少越界調(diào)用的一個關(guān)鍵因素是確保數(shù)據(jù)處于 Java/本機界限的正確一側(cè),。如果數(shù)據(jù)未在正確的一側(cè),,則另一側(cè)訪問數(shù)據(jù)的需求則會持續(xù)發(fā)起越界調(diào)用。

舉例來說,,如果我們希望使用 JNI 為某個串行端口提供接口,,則可以構(gòu)造兩種不同的接口。第一個版本如清單 6 所示:


清單 6. 到串行端口的接口:版本 1

				
/**
* Initializes the serial port and returns a java SerialPortConfig objects
* that contains the hardware address for the serial port, and holds
* information needed by the serial port such as the next buffer
* to write data into
*
* @param env JNI env that can be used by the method
* @param comPortName the name of the serial port
* @returns SerialPortConfig object to be passed ot setSerialPortBit
* and getSerialPortBit calls
*/
jobject initializeSerialPort(JNIEnv* env, jobject obj, jstring comPortName);

/**
* Sets a single bit in an 8 bit byte to be sent by the serial port
*
* @param env JNI env that can be used by the method
* @param serialPortConfig object returned by initializeSerialPort
* @param whichBit value from 1-8 indicating which bit to set
* @param bitValue 0th bit contains bit value to be set
*/
void setSerialPortBit(JNIEnv* env, jobject obj, jobject serialPortConfig,
jint whichBit, jint bitValue);

/**
* Gets a single bit in an 8 bit byte read from the serial port
*
* @param env JNI env that can be used by the method
* @param serialPortConfig object returned by initializeSerialPort
* @param whichBit value from 1-8 indicating which bit to read
* @returns the bit read in the 0th bit of the jint
*/
jint getSerialPortBit(JNIEnv* env, jobject obj, jobject serialPortConfig,
jint whichBit);

/**
* Read the next byte from the serial port
*
* @param env JNI env that can be used by the method
*/
void readNextByte(JNIEnv* env, jobject obj);

/**
* Send the next byte
*
* @param env JNI env that can be used by the method
*/
void sendNextByte(JNIEnv* env, jobject obj);

 

在 清單 6 中,串行端口的所有配置數(shù)據(jù)都存儲在由 initializeSerialPort() 方法返回的 Java 對象中,,并且將 Java 代碼完全控制對硬件中各數(shù)據(jù)位的設(shè)置,。清單 6 所示版本的一些問題會造成其性能差于清單 7 中的版本:


清單 7. 到串行端口的接口:版本 2

				
/**
* Initializes the serial port and returns an opaque handle to a native
* structure that contains the hardware address for the serial port
* and holds information needed by the serial port such as
* the next buffer to write data into
*
* @param env JNI env that can be used by the method
* @param comPortName the name of the serial port
* @returns opaque handle to be passed to setSerialPortByte and
* getSerialPortByte calls
*/
jlong initializeSerialPort2(JNIEnv* env, jobject obj, jstring comPortName);

/**
* sends a byte on the serial port
*
* @param env JNI env that can be used by the method
* @param serialPortConfig opaque handle for the serial port
* @param byte the byte to be sent
*/
void sendSerialPortByte(JNIEnv* env, jobject obj, jlong serialPortConfig,
jbyte byte);

/**
* Reads the next byte from the serial port
*
* @param env JNI env that can be used by the method
* @param serialPortConfig opaque handle for the serial port
* @returns the byte read from the serial port
*/
jbyte readSerialPortByte(JNIEnv* env, jobject obj, jlong serialPortConfig);

 

性能技巧 #6

構(gòu)造應(yīng)用程序的數(shù)據(jù),使它位于界限的正確的側(cè),,并且可以由使用它的代碼訪問,,而不需要大量跨界調(diào)用。

最顯著的一個問題就是,,清單 6 中的接口在設(shè)置或檢索每個位,,以及從串行端口讀取字節(jié)或者向串行端口寫入字節(jié)都需要一個 JNI 調(diào)用。這會導致讀取或?qū)懭氲拿總€字節(jié)的 JNI 調(diào)用變成原來的 9 倍,。第二個問題是,,清單 6 將串行端口的配置信息存儲在 Java/本機界限的錯誤一側(cè)的某個 Java 對象上。我們僅在本機側(cè)需要此配置數(shù)據(jù),;將它存儲在 Java 側(cè)會導致本機代碼向 Java 代碼發(fā)起大量回調(diào)以獲取/設(shè)置此配置信息,。清單 7 將配置信息存儲在一個本機結(jié)構(gòu)中(比如,一個 struct),,并向 Java 代碼返回了一個不透明的句柄,,該句柄可以在后續(xù)調(diào)用中返回。這意味著,,當本機代碼正在運行時,,它可以直接訪問該結(jié)構(gòu),而不需要回調(diào) Java 代碼獲取串行端口硬件地址或下一個可用的緩沖區(qū)等信息,。因此,使用 清單 7 的實現(xiàn)的性能將大大改善,。

使用大量本地引用而未通知 JVM

JNI 函數(shù)返回的任何對象都會創(chuàng)建本地引用,。舉例來說,當您調(diào)用 GetObjectArrayElement() 時,,將返回對數(shù)組中對象的本地引用,。考慮清單 8 中的代碼在運行一個很大的數(shù)組時會使用多少本地引用:


清單 8. 創(chuàng)建本地引用

				
void workOnArray(JNIEnv* env, jobject obj, jarray array){
jint i;
jint count = (*env)->GetArrayLength(env, array);
for (i=0; i < count; i++) {
jobject element = (*env)->GetObjectArrayElement(env, array, i);
if((*env)->ExceptionOccurred(env)) {
break;
}

/* do something with array element */
}
}

 

每次調(diào)用 GetObjectArrayElement() 時都會為元素創(chuàng)建一個本地引用,,并且直到本機代碼運行完成時才會釋放,。數(shù)組越大,所創(chuàng)建的本地引用就越多,。

性能技巧 #7

當本機代碼造成創(chuàng)建大量本地引用時,,在各引用不再需要時刪除它們。

這些本地引用會在本機方法終止時自動釋放,。JNI 規(guī)范要求各本機代碼至少能創(chuàng)建 16 個本地引用,。雖然這對許多方法來說都已經(jīng)足夠了,但一些方法在其生存期中卻需要更多的本地引用。對于這種情況,,您應(yīng)該刪除不再需要的引用,,方法是使用 JNI DeleteLocalRef() 調(diào)用,或者通知 JVM 您將使用更多的本地引用,。

清單 9 向 清單 8 中的示例添加了一個 DeleteLocalRef() 調(diào)用,,用于通知 JVM 本地引用已不再需要,以及將可同時存在的本地引用的數(shù)量限制為一個合理的數(shù)值,,而與數(shù)組的大小無關(guān):


清單 9. 添加 DeleteLocalRef()

				
void workOnArray(JNIEnv* env, jobject obj, jarray array){
jint i;
jint count = (*env)->GetArrayLength(env, array);
for (i=0; i < count; i++) {
jobject element = (*env)->GetObjectArrayElement(env, array, i);
if((*env)->ExceptionOccurred(env)) {
break;
}

/* do something with array element */

(*env)->DeleteLocalRef(env, element);
}
}

 

性能技巧 #8

如果某本機代碼將同時存在大量本地引用,,則調(diào)用 JNI EnsureLocalCapacity() 方法通知 JVM 并允許它優(yōu)化對本地引用的處理。

您可以調(diào)用 JNI EnsureLocalCapacity() 方法來通知 JVM 您將使用超過 16 個本地引用,。這將允許 JVM 優(yōu)化對該本機代碼的本地引用的處理,。如果無法創(chuàng)建所需的本地引用,或者 JVM 采用的本地引用管理方法與所使用的本地引用數(shù)量之間不匹配造成了性能低下,,則未成功通知 JVM 會導致 FatalError,。

 




回頁首



正確性缺陷

5 大 JNI 正確性缺陷包括:

  • 使用 錯誤的 JNIEnv
  • 未檢測異常
  • 未 檢測返回值
  • 未正確使 用數(shù)組方法
  • 未 正確使用全局引用

使用錯誤的 JNIEnv

執(zhí)行本機代碼的線程使用 JNIEnv 發(fā)起 JNI 方法調(diào)用。但是,,JNIEnv 并不是僅僅用于分派所請求的方法,。JNI 規(guī)范規(guī)定每個 JNIEnv 對于線程來說都是本地的。JVM 可以依賴于這一假設(shè),,將額外的線程本地信息存儲在 JNIEnv 中,。一個線程使用另一個線程中的 JNIEnv 會導致一些小 bug 和難以調(diào)試的崩潰問題。

正確性技巧 #1

僅在相關(guān)的單一線程中使用 JNIEnv,。

線程可以調(diào)用通過 JavaVM 對象使用 JNI 調(diào)用接口的 GetEnv() 來獲取 JNIEnv,。JavaVM 對象本身可以通過使用 JNIEnv 方法調(diào)用 JNI GetJavaVM() 來獲取,并且可以被緩存以及跨線程共享,。緩存 JavaVM 對象的副本將允許任何能訪問緩存對象的線程在必要時獲取對它自己的 JNIEnv 訪問,。要實現(xiàn)最優(yōu)性能,線程應(yīng)該繞過 JNIEnv,,因為查找它有時會需要大量的工作,。

未檢測異常

本 機能調(diào)用的許多 JNI 方法都會引起與執(zhí)行線程相關(guān)的異常。當 Java 代碼執(zhí)行時,,這些異常會造成執(zhí)行流程發(fā)生變化,,這樣便會自動調(diào)用異常處理代碼。當某個本機方法調(diào)用某個 JNI 方法時會出現(xiàn)異常,,但檢測異常并采用適當措施的工作將由本機來完成,。一個常見的 JNI 缺陷是調(diào)用 JNI 方法而未在調(diào)用完成后測試異常。這會造成代碼有大量漏洞以及程序崩潰,。

舉例來說,,考慮調(diào)用 GetFieldID() 的代碼,,如果無法找到所請求的字段,則會出現(xiàn) NoSuchFieldError,。如果本機代碼繼續(xù)運行而未檢測異常,,并使用它認為應(yīng)該返回的字段 ID,則會造成程序崩潰,。舉例來說,,如果 Java 類經(jīng)過修改,導致 charField 字段不再存在,,則清單 10 中的代碼可能會造成程序崩潰 — 而不是拋出一個 NoSuchFieldError


清單 10. 未能檢測異常

				jclass objectClass;
jfieldID fieldID;
jchar result = 0;

objectClass = (*env)->GetObjectClass(env, obj);
fieldID = (*env)->GetFieldID(env, objectClass, "charField", "C");
result = (*env)->GetCharField(env, obj, fieldID);

 

正確性技巧 #2

在發(fā)起可能會導致異常的 JNI 調(diào)用后始終檢測異常,。

添加異常檢測代碼要比在事后嘗試調(diào)試崩潰簡單很多。經(jīng)常,,您只需要檢測是否出現(xiàn)了某個異常,,如果是則立即返回 Java 代碼以便拋出異常。然后,,使用常規(guī)的 Java 異常處理流程處理它或者顯示它,。舉例來說,清單 11 將檢測異常:


清單 11. 檢測異常

				jclass objectClass;
jfieldID fieldID;
jchar result = 0;

objectClass = (*env)->GetObjectClass(env, obj);
fieldID = (*env)->GetFieldID(env, objectClass, "charField", "C");
if((*env)->ExceptionOccurred(env)) {
return;
}

result = (*env)->GetCharField(env, obj, fieldID);

 

不檢測和清除異常會導致出現(xiàn)意外行為,。您可以確定以下代碼的問題嗎,?

fieldID = (*env)->GetFieldID(env, objectClass, "charField", "C");
if (fieldID == NULL){
fieldID = (*env)->GetFieldID(env, objectClass,"charField", "D");
}
return (*env)->GetIntField(env, obj, fieldID);

 

問題在于,盡管代碼處理了初始 GetFieldID() 未返回字段 ID 的情況,,但它并未清除 此調(diào)用將設(shè)置的異常,。因此,本機返回的結(jié)果會造成立即拋出一個異常,。

未檢測返回值

許多 JNI 方法都通過返回值來指示調(diào)用成功與否,。與未檢測異常相似,這也存在一個缺陷,,即代碼未檢測返回值卻假定調(diào)用成功而繼續(xù)運行,。對于大多數(shù) JNI 方法來說,它們都設(shè)置了返回值和異常狀態(tài),,這樣應(yīng)用程序更可以通過檢測異常狀態(tài)或返回值來判斷方法運行正常與否。

正確性技巧 #3

始終檢測 JNI 方法的返回值,,并包括用于處理錯誤的代碼路徑,。

您可以確定以下代碼的問題嗎?

clazz = (*env)->FindClass(env, "com/ibm/j9//HelloWorld");
method = (*env)->GetStaticMethodID(env, clazz, "main",
"([Ljava/lang/String;)V");
(*env)->CallStaticVoidMethod(env, clazz, method, NULL);

 

問題在于,,如果未發(fā)現(xiàn) HelloWorld 類,,或者如果 main() 不存在,則本機將造成程序崩潰,。

未正確使用數(shù)組方法

GetXXXArrayElements()ReleaseXXXArrayElements() 方法允許您請求任何元素,。同樣,,GetPrimitiveArrayCritical()ReleasePrimitiveArrayCritical(),、GetStringCritical()ReleaseStringCritical() 允許您請求數(shù)組元素或字符串字節(jié),,以最大限度降低直接指向數(shù)組或字符串的可能性。這些方法的使用存在兩個常見的缺陷,。其一,,忘記在 ReleaseXXX() 方法調(diào)用中提供更改。即便使用 Critical 版本,,也無法保證您能獲得對數(shù)組或字符串的直接引用,。一些 JVM 始終返回一個副本,并且在這些 JVM 中,,如果您在 ReleaseXXX() 調(diào)用中指定了 JNI_ABORT,,或者忘記調(diào)用了 ReleaseXXX(),則對數(shù)組的更改不會被復制回去,。

舉例來說,,考慮以下代碼:

void modifyArrayWithoutRelease(JNIEnv* env, jobject obj, jarray arr1) {
jboolean isCopy;
jbyte* buffer = (*env)-> (*env)->GetByteArrayElements(env,arr1,&isCopy);
if ((*env)->ExceptionCheck(env)) return;

buffer[0] = 1;
}

 

正確性技巧 #4

不要忘記為每個 GetXXX() 使用模式 0(復制回去并釋放內(nèi)存) 調(diào)用 ReleaseXXX()

在提供直接指向數(shù)組的指針的 JVM 上,,該數(shù)組將被更新,;但是,在返回副本的 JVM 上則不是如此,。這會造成您的代碼在一些 JVM 上能夠正常運行,,而在其他 JVM 上卻會出錯。您應(yīng)該始終始終包括一個釋放(release)調(diào)用,,如清單 12 所示:


清單 12. 包括一個釋放調(diào)用

				
void modifyArrayWithRelease(JNIEnv* env, jobject obj, jarray arr1) {
jboolean isCopy;
jbyte* buffer = (*env)-> (*env)->GetByteArrayElements(env,arr1,&isCopy);
if ((*env)->ExceptionCheck(env)) return;

buffer[0] = 1;

(*env)->ReleaseByteArrayElements(env, arr1, buffer, JNI_COMMIT);
if ((*env)->ExceptionCheck(env)) return;
}

 

第二個缺陷是不注重規(guī)范對在 GetXXXCritical()ReleaseXXXCritical() 之間執(zhí)行的代碼施加的限制,。本機可能不會在這些方法之間發(fā)起任何調(diào)用,并且可能不會由于任何原因而阻塞,。未重視這些限制會造成應(yīng)用程序或 JVM 中出現(xiàn)間斷性死鎖,。

舉例來說,以下代碼看上去可能沒有問題:

void workOnPrimitiveArray(JNIEnv* env, jobject obj, jarray arr1) {
jboolean isCopy;
jbyte* buffer = (*env)->GetPrimitiveArrayCritical(env, arr1, &isCopy);
if ((*env)->ExceptionCheck(env)) return;

processBufferHelper(buffer);

(*env)->ReleasePrimitiveArrayCritical(env, arr1, buffer, 0);
if ((*env)->ExceptionCheck(env)) return;
}

 

正確性技巧 #5

確保代碼不會在 GetXXXCritical()ReleaseXXXCritical() 調(diào)用之間發(fā)起任何 JNI 調(diào)用或由于任何原因出現(xiàn)阻塞,。

但是,,我們需要驗證在調(diào)用 processBufferHelper() 時可以運行的所有代碼都沒有違反任何限制。這些限制適用于在 GetRelease 調(diào)用之間執(zhí)行的所有代碼,,無論它是不是本機的一部分,。

未正確使用全局引用

本機可以創(chuàng)建一些全局引用,以保證對象在不再需要時才會被垃圾收集器回收,。常見的缺陷包括忘記刪除已創(chuàng)建的全局引用,,或者完全失去對它們的跟蹤??紤]一個本機創(chuàng)建了全局引用,,但是未刪除它或?qū)⑺鎯υ谀程帲?/p>

lostGlobalRef(JNIEnv* env, jobject obj, jobject keepObj) {
jobject gref = (*env)->NewGlobalRef(env, keepObj);
}

 



創(chuàng) 建全局引用時,,JVM 會將它添加到一個禁止垃圾收集的對象列表中。當本機返回時,,它不僅會釋放全局引用,,應(yīng)用程序還無法獲取引用以便稍后釋放它 — 因此,對象將會始終存在,。不釋放全局引用會造成各種問題,,不僅因為它們會保持對象本身為活動狀態(tài),還因為它們會將通過該對象能接觸到的所有對象都保持為活動狀態(tài),。在某些情況下,,這會顯著加劇內(nèi)存泄漏。



避免常見缺陷

假設(shè)您編寫了一些新 JNI 代碼,,或者繼承了別處的某些 JVI 代碼,,如何才能確保避免了常見缺陷,或者在繼承代碼中發(fā)現(xiàn)它們,?表 1 提供了一些確定這些常見缺陷的技巧:


表 1. 確定 JNI 編程缺陷的清單


未緩存 觸發(fā)數(shù)組副本 錯誤界限 過多回訪 使用大量本地引用 使用錯誤的 JNIEnv 未檢測異常 未 檢測返回值 未正確使用數(shù)組 未正確使用全局引用
規(guī)范驗證




X X
X
方法跟蹤 X X X X

X
X X
轉(zhuǎn)儲








X
-verbose:jni



X




代碼審查 X X X X X X X X X X

您可以在開發(fā)周期的早期確定許多常見缺陷,,方法如下:

  • 根據(jù)規(guī)范驗 證新代碼
  • 分析 方法跟蹤
  • 使 用 -verbose:jni 選項
  • 生成 轉(zhuǎn)儲
  • 執(zhí) 行代碼審查

根據(jù) JNI 規(guī)范驗證新代碼

維持規(guī)范的限制列表并審查本機與列表的遵從性是一個很好的實踐,這可以通過手動或自動代碼分析來完成,。確保遵從性的工作可能會比調(diào)試由于違背限制而出現(xiàn)的細小和間斷性故障輕松很多,。下面提供了一個專門針對新開發(fā)代碼(或?qū)δ鷣碚f是新的)的規(guī)范順從性檢查列表:

  • 驗證 JNIEnv 僅與與之相關(guān)的線程使用。
  • 確認未在 GetXXXCritical()ReleaseXXXCritical() 部分調(diào)用 JNI 方法,。
  • 對于進入關(guān)鍵部分的方法,,驗證該方法未在釋放前返回。
  • 驗證在所有可能引起異常的 JNI 調(diào)用之前都檢測了異常,。
  • 確保所有 Get/Release 調(diào)用在各 JNI 方法中都是相匹配的,。

IBM 的 JVM 實現(xiàn)包括開啟自動 JNI 檢測的選項,其代價是較慢的執(zhí)行速度,。與出色的代碼單元測試相結(jié)合,,這是一種極為強大的工具。您可以運行應(yīng)用程序或單元測試來執(zhí)行遵從性檢查,,或者確定所遇到的 bug 是否是由本機引起的,。除了執(zhí)行上述規(guī)范遵從性檢查之外,它還能確保:

  • 傳遞給 JNI 方法的參數(shù)屬于正確的類型,。
  • JNI 代碼未讀取超過數(shù)組結(jié)束部分之外的內(nèi)容,。
  • 傳遞給 JNI 方法的指針都是有效的。

JNI 檢測報告的所有結(jié)論并不一定都是代碼中的錯誤,。它們還包括一些針對代碼的建議,,您應(yīng)該仔細閱讀它們以確保代碼功能正常,。

您可以通過以下命令行啟用 JNI 檢測選項:

Usage: -Xcheck:jni:[option[,option[,...]]]

all check application and system classes
verbose trace certain JNI functions and activities
trace trace all JNI functions
nobounds do not perform bounds checking on strings and arrays
nonfatal do not exit when errors are detected
nowarn do not display warnings
noadvice do not display advice
novalist do not check for va_list reuse
valist check for va_list reuse
pedantic perform more thorough, but slower checks
help print this screen

 

使用 IBM JVM 的 -Xcheck:jni 選項作為標準開發(fā)流程的一部分可以幫助您更加輕松地找出代碼錯誤,。特別是,,它可以幫助您確定在錯誤線程中使用 JNIEnv 以及未正確使用關(guān)鍵區(qū)域的缺陷的根源。

最新的 Sun JVM 提供了一個類似的 -Xcheck:jni 選項,。它的工作原理不同于 IBM 版本,,并且提供了不同的信息,但是它們的作用是相同的,。它會在發(fā)現(xiàn)未符合規(guī)范的代碼時發(fā)出警告,,并且可以幫助您確定常見的 JNI 缺陷。

分析方法跟蹤

生成對已調(diào)用本機方法以及這些本機方法發(fā)起的 JNI 回調(diào)的跟蹤,,這對確定大量常見缺陷的根源是非常有用的,。可確定的問題包括:

  • 大量 GetFieldID()GetMethodID() 調(diào)用 — 特別是,,如果這些調(diào)用針對相同的字段和方法 — 表示字段和方法未被緩存,。

  • GetTypeArrayElements() 調(diào)用實例(而非 GetTypeArrayRegion()) 有時表示存在不必要的復制。

  • 在 Java 代碼與本機代碼之前來回快速切換(由時間戳指示)有時表示 Java 代碼與本機代碼之間的界限有誤,,從而造成性能較差,。

  • 每個本機函數(shù)調(diào)用后面都緊接著大量 GetFieldID() 調(diào)用,這種模式表示并未傳遞所需的參數(shù),,而是強制本機回訪完成工作所需的數(shù)據(jù),。

  • 調(diào)用可能拋出異常的 JNI 方法之后缺少對 ExceptionOccurred()ExceptionCheck() 的調(diào)用表示本機未正確檢測異常。

  • GetXXX()ReleaseXXX() 方法調(diào)用的數(shù)量不匹配表示缺少釋放操作,。

  • GetXXXCritical()ReleaseXXXCritical() 調(diào)用之間調(diào)用 JNI 方法表示未遵循規(guī)范施加的限制,。

  • 如果調(diào)用 GetXXXCritical()ReleaseXXXCritical() 之間相隔的時間較長,則表示未遵循 “不要阻塞調(diào)用” 規(guī)范所施加的限制,。

  • NewGlobalRef()DeleteGlobalRef() 調(diào)用之間出現(xiàn)嚴重失衡表示釋放不再需要的引用時出現(xiàn)故障,。

一些 JVM 實現(xiàn)提供了一種可用于生存方法跟蹤的機制。您還可以通過各種外部工具來生成跟蹤,,比如探查器和代碼覆蓋工具,。

IBM JVM 實現(xiàn)提供了許多用于生成跟蹤信息的方法。第一種方法是使用 -Xcheck:jni:trace 選項,。這將生成對已調(diào)用的本機方法以及它們發(fā)起的 JNI 回調(diào)的跟蹤,。清單 13 顯示某個跟蹤的摘錄(為便于閱讀,隔開了某些行):


清單 13. IBM JVM 實現(xiàn)所生成的方法跟蹤

				
Call JNI: java/lang/System.getPropertyList()[Ljava/lang/String; {
00177E00 Arguments: void
00177E00 FindClass("java/lang/String")
00177E00 FindClass("com/ibm/oti/util/Util")
00177E00 Call JNI: com/ibm/oti/vm/VM.useNativesImpl()Z {
00177E00 Arguments: void
00177E00 Return: (jboolean)false
00177E00 }
00177E00 Call JNI: java/security/AccessController.initializeInternal()V {
00177E00 Arguments: void
00177E00 FindClass("java/security/AccessController")
00177E00 GetStaticMethodID(java/security/AccessController, "doPrivileged",
"(Ljava/security/PrivilegedAction;)Ljava/lang/Object;")
00177E00 GetStaticMethodID(java/security/AccessController, "doPrivileged",
"(Ljava/security/PrivilegedExceptionAction;)Ljava/lang/Object;")
00177E00 GetStaticMethodID(java/security/AccessController, "doPrivileged",
"(Ljava/security/PrivilegedAction;Ljava/security/AccessControlContext;)
Ljava/lang/Object;")
00177E00 GetStaticMethodID(java/security/AccessController, "doPrivileged",
"(Ljava/security/PrivilegedExceptionAction;
Ljava/security/AccessControlContext;)Ljava/lang/Object;")
00177E00 Return: void
00177E00 }
00177E00 GetStaticMethodID(com/ibm/oti/util/Util, "toString",
"([BII)Ljava/lang/String;")
00177E00 NewByteArray((jsize)256)
00177E00 NewObjectArray((jsize)118, java/lang/String, (jobject)NULL)
00177E00 SetByteArrayRegion([B@0018F7D0, (jsize)0, (jsize)30, (void*)7FF2E1D4)
00177E00 CallStaticObjectMethod/CallStaticObjectMethodV(com/ibm/oti/util/Util,
toString([BII)Ljava/lang/String;, (va_list)0007D758) {
00177E00 Arguments: (jobject)0x0018F7D0, (jint)0, (jint)30
00177E00 Return: (jobject)0x0018F7C8
00177E00 }
00177E00 ExceptionCheck()

 

清單 13 中的跟蹤摘錄顯示了已調(diào)用的本機方法(比如 AccessController.initializeInternal()V)以及本機方法發(fā)起的 JNI 回調(diào),。

使用 -verbose:jni 選項

Sun 和 IBM JVM 還提供了一個 -verbose:jni 選項,。對于 IBM JVM 而言,開啟此選項將提供關(guān)于當前 JNI 回調(diào)的信息,。清單 14 顯示了一個示例:


清單 14. 使用 IBM JVM 的 -verbose:jni 列出 JNI 回調(diào)

				
<JNI GetStringCritical: buffer=0x100BD010>
<JNI ReleaseStringCritical: buffer=100BD010>
<JNI GetStringChars: buffer=0x03019C88>
<JNI ReleaseStringChars: buffer=03019C88>
<JNI FindClass: java/lang/String>
<JNI FindClass: java/io/WinNTFileSystem>
<JNI GetMethodID: java/io/WinNTFileSystem.<init> ()V>
<JNI GetStaticMethodID: com/ibm/j9/offload/tests/HelloWorld.main ([Ljava/lang/String;)V>
<JNI GetMethodID: java/lang/reflect/Method.getModifiers ()I>
<JNI FindClass: java/lang/String>

 

對于 Sun JVM 而言,,開啟 -verbose:jni 選項不會提供關(guān)于當前調(diào)用的信息,但它會提供關(guān)于所使用的本機方法的額外信息,。清單 15 顯示了一個示例:


清單 15. 使用 Sun JVM 的 -verbose:jni

				
[Dynamic-linking native method java.util.zip.ZipFile.getMethod ... JNI]
[Dynamic-linking native method java.util.zip.Inflater.initIDs ... JNI]
[Dynamic-linking native method java.util.zip.Inflater.init ... JNI]
[Dynamic-linking native method java.util.zip.Inflater.inflateBytes ... JNI]
[Dynamic-linking native method java.util.zip.ZipFile.read ... JNI]
[Dynamic-linking native method java.lang.Package.getSystemPackage0 ... JNI]
[Dynamic-linking native method java.util.zip.Inflater.reset ... JNI]

 

開啟此選項還會讓 JVM 針對使用過多本地引用而未通知 JVM 的情況發(fā)起警告,。舉例來說,,IBM JVM 生成了這樣一個消息:

JVMJNCK065W JNI warning in FindClass: Automatically grew local reference frame capacity 
from 16 to 48. 17 references are in use.
Use EnsureLocalCapacity or PushLocalFrame to explicitly grow the frame.

 

雖然 -verbose:jni-Xcheck:jni:trace 選項可幫助您方便地獲取所需的信息,但手動審查此信息是一項艱巨的任務(wù),。一個不錯的提議是,,創(chuàng)建一些腳本或?qū)嵱霉ぞ邅硖幚碛?JVM 生成的跟蹤文件,并查看 警告,。

生成轉(zhuǎn)儲

運行中的 Java 進程生成的轉(zhuǎn)儲包含大量關(guān)于 JVM 狀態(tài)的信息,。對于許多 JVM 來說,它們包括關(guān)于全局引用的信息,。舉例來說,,最新的 Sun JVM 在轉(zhuǎn)儲信息中包括這樣一行:

JNI global references: 73

 

通過生成前后轉(zhuǎn)儲,您可以確定是否創(chuàng)建了任何未正常釋放的全局引用,。

您可以在 UNIX? 環(huán)境中通過對 java 進程發(fā)起 kill -3kill -QUIT 來請求轉(zhuǎn)儲,。在 Windows? 上,使用 Ctrl+Break 組合鍵,。

對于 IBM JVM,,使用以下步驟獲取關(guān)于全局引用的信息:

  1. -Xdump:system:events=user 添加到命令行。這樣,,當您在 UNIX 系統(tǒng)上調(diào)用 kill -3 或者在 Windows 上按下 Ctrl+Break 時,,JVM 便會生成轉(zhuǎn)儲。

  2. 程序在運行中時會生成后續(xù)轉(zhuǎn)儲,。

  3. 運行 jextract -nozip core.XXX output.xml,,這將會將轉(zhuǎn)儲信 息提取到可讀格式的 output.xml 中。

  4. 查找 output.xml 中的 JNIGlobalReference 條目,,它提供關(guān)于當前全局引用的信息,,如清單 16 所示:


清單 16. output.xml 中的 JNIGlobalReference 條目

				
<rootobject type="Thread" id="0x10089990" reachability="strong" />
<rootobject type="Thread" id="0x10089fd0" reachability="strong" />
<rootobject type="JNIGlobalReference" id="0x100100c0" reachability="strong" />
<rootobject type="JNIGlobalReference" id="0x10011250" reachability="strong" />
<rootobject type="JNIGlobalReference" id="0x10011840" reachability="strong" />
<rootobject type="JNIGlobalReference" id="0x10011880" reachability="strong" />
<rootobject type="JNIGlobalReference" id="0x10010af8" reachability="strong" />
<rootobject type="JNIGlobalReference" id="0x10010360" reachability="strong" />
<rootobject type="JNIGlobalReference" id="0x10081f48" reachability="strong" />

<rootobject type="StringTable" id="0x10010be0" reachability="weak" />
<rootobject type="StringTable" id="0x10010c70" reachability="weak" />
<rootobject type="StringTable" id="0x10010d00" reachability="weak" />
<rootobject type="StringTable" id="0x10011018" reachability="weak" />

 

通過查看后續(xù) Java 轉(zhuǎn)儲中報告的數(shù)值,您可以確定全局引用是否出現(xiàn)的泄漏,。

參見 參考資料 獲取關(guān)于使用轉(zhuǎn)儲文件以及 IBM JVM 的 jextract 的更多信息,。

執(zhí)行代碼審查

代碼審查經(jīng)常可用于確定常見缺陷,,并且可以在各種級別上完成,。繼承新代碼時,快速掃描可以發(fā)現(xiàn)各種問題,,從而避免稍后花費更多時間進行調(diào)試,。在某些情況下,審查是確定缺陷實例(比如未檢查返回值)的唯一方法,。舉例來說,,此代碼的問題可能可以通過代碼審查輕松確定,但卻很難通過調(diào)試來發(fā)現(xiàn):

int calledALot(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");

}

jclass getObjectClassHelper(jobject object){
/* use globally cached JNIEnv */
return cls = (*globalEnvStatic)->GetObjectClass(globalEnvStatic,allValues);
}

 

代碼審查可能會發(fā)現(xiàn)第一個方法未正確緩存字段 ID,盡管重復使用了相同的 ID,,并且第二個方法所使用的 JNIEnv 并不在應(yīng)該在的線程上,。

結(jié)束語

現(xiàn)在,您已經(jīng)了解了 10 大 JNI 編程缺陷,,以及一些用于在已有或新代碼中確定它們的良好實踐。堅持應(yīng)用這些實踐有助于提高 JNI 代碼的正確率,,并且您的應(yīng)用程序可以實現(xiàn)所需的性能水平,。

有 效集成已有代碼資源的能力對于面向?qū)ο蠹軜?gòu)(SOA)和基于云的計算這兩種技術(shù)的成功至關(guān)重要。JNI 是一項非常重要的技術(shù),,用于將非 Java 舊有代碼和組件集成到基于 Java 的平臺中,,充當 SOA 或基于云的系統(tǒng)的基本元素。正確使用 JNI 可以加速將這些組件轉(zhuǎn)變?yōu)榉?wù)的過程,,并允許您從現(xiàn)有投資中獲得最大優(yōu)勢,。

    本站是提供個人知識管理的網(wǎng)絡(luò)存儲空間,所有內(nèi)容均由用戶發(fā)布,,不代表本站觀點,。請注意甄別內(nèi)容中的聯(lián)系方式、誘導購買等信息,,謹防詐騙,。如發(fā)現(xiàn)有害或侵權(quán)內(nèi)容,請點擊一鍵舉報,。
    轉(zhuǎn)藏 分享 獻花(0

    0條評論

    發(fā)表

    請遵守用戶 評論公約

    類似文章 更多