Java? 本機接口(Java Native Interface,JNI)是一個標準的 Java API,它支持將 Java 代碼與使用其他編程語言編寫的代碼相集成。如果您希望利用已有的代碼資源,那么可以使用 JNI 作為您工具包中的關(guān)鍵組件 —— 比如在面向服務(wù)架構(gòu)(SOA)和基于云的系統(tǒng)中。但是,如果在使用時未注意某些事項,則 JNI 會迅速導(dǎo)致應(yīng)用程序性能低下且不穩(wěn)定。本文將確定 10 大 JNI 編程缺陷,提供避免這些缺陷的最佳實踐,并介紹可用于實現(xiàn)這些實踐的工具。
Java 環(huán)境和語言對于應(yīng)用程序開發(fā)來說是非常安全和高效的。但是,一些應(yīng)用程序卻需要執(zhí)行純 Java 程序無法完成的一些任務(wù),比如:
ping
時,您可能需要 Internet Control Message Protocol (ICMP) 功能,但基本類庫并未提供它。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 的靈活性和強大性帶來了一些編程實踐上的風險,比如導(dǎo)致性能較差、出現(xiàn) bug 甚至程序崩潰。您必須格外留意應(yīng)用程序中的代碼,并使用良好的實踐來保障應(yīng)用程序的總體完整性。
本文介紹 JNI 用戶最常遇到的 10 大編碼和設(shè)計錯誤。其目標是幫助您認識到并避免它們,以便您可以編寫安全、高效、性能出眾的 JNI 代碼。本文還將介紹一些用于在新代碼或已有代碼中查找這些問題的工具和技巧,并展示如何有效地應(yīng)用它們。
JNI 編程缺陷可以分為兩類:
性能缺陷
程序員在使用 JNI 時的 5 大性能缺陷如下:
不緩存方法 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 對于特定類是相同的,因此您只需要查找一次,然后便可重復(fù)使用。同樣,查找類對象的開銷也很大,因此也應(yīng)該緩存它們。
舉例來說,清單 1 展示了調(diào)用靜態(tài)方法所需的 JNI 代碼:
清單 1. 使用 JNI 調(diào)用靜態(tài)方法
|
當我們每次希望調(diào)用方法時查找類和方法 ID 都會產(chǎn)生六個本機調(diào)用,而不是第一次緩存類和方法 ID 時需要的兩個調(diào)用。
緩存會對您應(yīng)用程序的運行時造成顯著的影響??紤]下面兩個版本的方法,它們的作用是相同的。清單 2 使用了緩存的字段 ID:
清單 2. 使用緩存的字段 ID
|
![]() |
|
清單 3 沒有使用緩存的字段 ID:
清單 3. 未緩存字段 ID
|
清單 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)用可以復(fù)制被操作的元素。舉例來說,如果您對含有 1,000 個元素的數(shù)組調(diào)用 GetLongArrayElements()
,則會造成至少分配或復(fù)制 8,000 字節(jié)的數(shù)據(jù)(每個 long
1,000 元素 * 8 字節(jié))。當您隨后使用 ReleaseLongArrayElements()
更新數(shù)組的內(nèi)容時,需要另外復(fù)制 8,000 字節(jié)的數(shù)據(jù)來更新數(shù)組。即使您使用較新的 GetPrimitiveArrayCritical()
,規(guī)范仍然準許 JVM 創(chuàng)建完整數(shù)組的副本。
![]() |
|
GetTypeArrayRegion()
和 SetTypeArrayRegion()
方法允許您獲取和更新數(shù)組的一部分,而不是整個數(shù)組。通過使用這些方法訪問較大的數(shù)組,您可以確保只復(fù)制本機代碼將要實際使用的數(shù)組部分。
舉例來說,考慮相同方法的兩個版本,如清單 4 所示:
清單 4. 相同方法的兩個版本
|
第一個版本可以生成兩個完整的數(shù)組副本,而第二個版本則完全沒有復(fù)制數(shù)組。當數(shù)組大小為 1,000 字節(jié)時,運行第一個方法 10,000,000 次用了 12,055 ms;而第二個版本僅用了 1,421 ms。第一個版本多花了 8.5 倍的時間!
![]() |
|
另一方面,如果您最終要獲取數(shù)組中的所有元素,則使用 GetTypeArrayRegion()
逐個獲取數(shù)組中的元素是得不償失的。要獲取最佳的性能,應(yīng)該確保以盡可能大的塊的來獲取和更新數(shù)組元素。如果您要迭代一個數(shù)組中的所有元素,則 清單 4 中這兩個 getElement()
方法都不適用。比較好的方法是在一個調(diào)用中獲取大小合理的數(shù)組部分,然后再迭代所有這些元素,重復(fù)操作直到覆蓋整個數(shù)組。
回訪而不是傳遞參數(shù)
在調(diào)用某個方法時,您經(jīng)常會在傳遞一個有多個字段的對象以及單獨傳遞字段之間做出選擇。在面向?qū)ο笤O(shè)計中,傳遞對象通常能提供較好的封裝,因為對象字段的變化不需要改變方法簽名。但是,對于 JNI 來說,本機代碼必須通過一個或多個 JNI 調(diào)用返回到 JVM 以獲取需要的各個字段的值。這些額外的調(diào)用會帶來額外的開銷,因為從本機代碼過渡到 Java 代碼要比普通方法調(diào)用開銷更大。因此,對于 JNI 來說,本機代碼從傳遞進來的對象中訪問大量單獨字段時會導(dǎo)致性能降低。
考慮清單 5 中的兩個方法,第二個方法假定我們緩存了字段 ID:
清單 5. 兩個方法版本
|
![]() |
|
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 代碼也需要耗費大量的時間。
![]() |
|
因 此,在設(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
|
在 清單 6 中,串行端口的所有配置數(shù)據(jù)都存儲在由 initializeSerialPort()
方法返回的 Java 對象中,并且將 Java 代碼完全控制對硬件中各數(shù)據(jù)位的設(shè)置。清單 6 所示版本的一些問題會造成其性能差于清單 7 中的版本:
清單 7. 到串行端口的接口:版本 2
|
![]() |
|
最顯著的一個問題就是,清單 6 中的接口在設(shè)置或檢索每個位,以及從串行端口讀取字節(jié)或者向串行端口寫入字節(jié)都需要一個 JNI 調(diào)用。這會導(dǎo)致讀取或?qū)懭氲拿總€字節(jié)的 JNI 調(diào)用變成原來的 9 倍。第二個問題是,清單 6 將串行端口的配置信息存儲在 Java/本機界限的錯誤一側(cè)的某個 Java 對象上。我們僅在本機側(cè)需要此配置數(shù)據(jù);將它存儲在 Java 側(cè)會導(dǎo)致本機代碼向 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)建本地引用
|
每次調(diào)用 GetObjectArrayElement()
時都會為元素創(chuàng)建一個本地引用,并且直到本機代碼運行完成時才會釋放。數(shù)組越大,所創(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()
|
![]() |
|
您可以調(diào)用 JNI EnsureLocalCapacity()
方法來通知 JVM 您將使用超過 16 個本地引用。這將允許 JVM 優(yōu)化對該本機代碼的本地引用的處理。如果無法創(chuàng)建所需的本地引用,或者 JVM 采用的本地引用管理方法與所使用的本地引用數(shù)量之間不匹配造成了性能低下,則未成功通知 JVM 會導(dǎo)致 FatalError
。
![]() ![]() |
![]()
|
正確性缺陷
5 大 JNI 正確性缺陷包括:
JNIEnv
使用錯誤的 JNIEnv
執(zhí)行本機代碼的線程使用 JNIEnv
發(fā)起 JNI 方法調(diào)用。但是,JNIEnv
并不是僅僅用于分派所請求的方法。JNI 規(guī)范規(guī)定每個 JNIEnv
對于線程來說都是本地的。JVM 可以依賴于這一假設(shè),將額外的線程本地信息存儲在 JNIEnv
中。一個線程使用另一個線程中的 JNIEnv
會導(dǎo)致一些小 bug 和難以調(diào)試的崩潰問題。
![]() |
|
線程可以調(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)過修改,導(dǎo)致 charField
字段不再存在,則清單 10 中的代碼可能會造成程序崩潰 — 而不是拋出一個 NoSuchFieldError
:
清單 10. 未能檢測異常
jclass objectClass; |
![]() |
|
添加異常檢測代碼要比在事后嘗試調(diào)試崩潰簡單很多。經(jīng)常,您只需要檢測是否出現(xiàn)了某個異常,如果是則立即返回 Java 代碼以便拋出異常。然后,使用常規(guī)的 Java 異常處理流程處理它或者顯示它。舉例來說,清單 11 將檢測異常:
清單 11. 檢測異常
jclass objectClass; |
不檢測和清除異常會導(dǎo)致出現(xiàn)意外行為。您可以確定以下代碼的問題嗎?
fieldID = (*env)->GetFieldID(env, objectClass, "charField", "C"); |
問題在于,盡管代碼處理了初始 GetFieldID()
未返回字段 ID 的情況,但它并未清除 此調(diào)用將設(shè)置的異常。因此,本機返回的結(jié)果會造成立即拋出一個異常。
未檢測返回值
許多 JNI 方法都通過返回值來指示調(diào)用成功與否。與未檢測異常相似,這也存在一個缺陷,即代碼未檢測返回值卻假定調(diào)用成功而繼續(xù)運行。對于大多數(shù) JNI 方法來說,它們都設(shè)置了返回值和異常狀態(tài),這樣應(yīng)用程序更可以通過檢測異常狀態(tài)或返回值來判斷方法運行正常與否。
![]() |
|
您可以確定以下代碼的問題嗎?
clazz = (*env)->FindClass(env, "com/ibm/j9//HelloWorld"); |
問題在于,如果未發(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ù)組的更改不會被復(fù)制回去。
舉例來說,考慮以下代碼:
void modifyArrayWithoutRelease(JNIEnv* env, jobject obj, jarray arr1) { |
![]() |
|
在提供直接指向數(shù)組的指針的 JVM 上,該數(shù)組將被更新;但是,在返回副本的 JVM 上則不是如此。這會造成您的代碼在一些 JVM 上能夠正常運行,而在其他 JVM 上卻會出錯。您應(yīng)該始終始終包括一個釋放(release)調(diào)用,如清單 12 所示:
清單 12. 包括一個釋放調(diào)用
|
第二個缺陷是不注重規(guī)范對在 GetXXXCritical()
和 ReleaseXXXCritical()
之間執(zhí)行的代碼施加的限制。本機可能不會在這些方法之間發(fā)起任何調(diào)用,并且可能不會由于任何原因而阻塞。未重視這些限制會造成應(yīng)用程序或 JVM 中出現(xiàn)間斷性死鎖。
舉例來說,以下代碼看上去可能沒有問題:
void workOnPrimitiveArray(JNIEnv* env, jobject obj, jarray arr1) { |
![]() |
|
但是,我們需要驗證在調(diào)用 processBufferHelper()
時可以運行的所有代碼都沒有違反任何限制。這些限制適用于在 Get
和 Release
調(diào)用之間執(zhí)行的所有代碼,無論它是不是本機的一部分。
未正確使用全局引用
本機可以創(chuàng)建一些全局引用,以保證對象在不再需要時才會被垃圾收集器回收。常見的缺陷包括忘記刪除已創(chuàng)建的全局引用,或者完全失去對它們的跟蹤??紤]一個本機創(chuàng)建了全局引用,但是未刪除它或?qū)⑺鎯υ谀程帲?/p>
lostGlobalRef(JNIEnv* env, jobject obj, jobject 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ā)周期的早期確定許多常見缺陷,方法如下:
-verbose:jni
選項 根據(jù) JNI 規(guī)范驗證新代碼
維持規(guī)范的限制列表并審查本機與列表的遵從性是一個很好的實踐,這可以通過手動或自動代碼分析來完成。確保遵從性的工作可能會比調(diào)試由于違背限制而出現(xiàn)的細小和間斷性故障輕松很多。下面提供了一個專門針對新開發(fā)代碼(或?qū)δ鷣碚f是新的)的規(guī)范順從性檢查列表:
JNIEnv
僅與與之相關(guān)的線程使用。 GetXXXCritical()
的 ReleaseXXXCritical()
部分調(diào)用 JNI 方法。 Get
/Release
調(diào)用在各 JNI 方法中都是相匹配的。 IBM 的 JVM 實現(xiàn)包括開啟自動 JNI 檢測的選項,其代價是較慢的執(zhí)行速度。與出色的代碼單元測試相結(jié)合,這是一種極為強大的工具。您可以運行應(yīng)用程序或單元測試來執(zhí)行遵從性檢查,或者確定所遇到的 bug 是否是由本機引起的。除了執(zhí)行上述規(guī)范遵從性檢查之外,它還能確保:
JNI 檢測報告的所有結(jié)論并不一定都是代碼中的錯誤。它們還包括一些針對代碼的建議,您應(yīng)該仔細閱讀它們以確保代碼功能正常。
您可以通過以下命令行啟用 JNI 檢測選項:
Usage: -Xcheck:jni:[option[,option[,...]]] |
使用 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()
) 有時表示存在不必要的復(fù)制。GetFieldID()
調(diào)用,這種模式表示并未傳遞所需的參數(shù),而是強制本機回訪完成工作所需的數(shù)據(jù)。ExceptionOccurred()
或 ExceptionCheck()
的調(diào)用表示本機未正確檢測異常。GetXXX()
和 ReleaseXXX()
方法調(diào)用的數(shù)量不匹配表示缺少釋放操作。GetXXXCritical()
和 ReleaseXXXCritical()
調(diào)用之間調(diào)用 JNI 方法表示未遵循規(guī)范施加的限制。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)所生成的方法跟蹤
|
清單 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)
|
對于 Sun JVM 而言,開啟 -verbose:jni
選項不會提供關(guān)于當前調(diào)用的信息,但它會提供關(guān)于所使用的本機方法的額外信息。清單 15 顯示了一個示例:
清單 15. 使用 Sun JVM 的 -verbose:jni
|
開啟此選項還會讓 JVM 針對使用過多本地引用而未通知 JVM 的情況發(fā)起警告。舉例來說,IBM JVM 生成了這樣一個消息:
JVMJNCK065W JNI warning in FindClass: Automatically grew local reference frame capacity |
雖然 -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 -3
或 kill -QUIT
來請求轉(zhuǎn)儲。在 Windows? 上,使用 Ctrl+Break 組合鍵。
對于 IBM JVM,使用以下步驟獲取關(guān)于全局引用的信息:
-Xdump:system:events=user
添加到命令行。這樣,當您在 UNIX 系統(tǒng)上調(diào)用 kill -3
或者在 Windows 上按下 Ctrl+Break 時,JVM 便會生成轉(zhuǎn)儲。jextract -nozip core.XXX output.xml
,這將會將轉(zhuǎn)儲信 息提取到可讀格式的 output.xml 中。JNIGlobalReference
條目,它提供關(guān)于當前全局引用的信息,如清單 16 所示:
清單 16. output.xml 中的 JNIGlobalReference
條目
|
通過查看后續(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){ |
代碼審查可能會發(fā)現(xiàn)第一個方法未正確緩存字段 ID,盡管重復(fù)使用了相同的 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)勢。