使用 Java Native Interface 的最佳實踐

發表於2012-07-24

來源:IBM developerworks

簡介: Java™ 本機介面(Java Native Interface,JNI)是一個標準的 Java API,它支援將 Java 程式碼與使用其他程式語言編寫的程式碼相整合。如果您希望利用已有的程式碼資源,那麼可以使用 JNI 作為您工具包中的關鍵元件 —— 比如在面向服務架構(SOA)和基於雲的系統中。但是,如果在使用時未注意某些事項,則 JNI 會迅速導致應用程式效能低下且不穩定。本文將確定 10 大 JNI 程式設計缺陷,提供避免這些缺陷的最佳實踐,並介紹可用於實現這些實踐的工具。

Java 環境和語言對於應用程式開發來說是非常安全和高效的。但是,一些應用程式卻需要執行純 Java 程式無法完成的一些任務,比如:

JNI 的發展

JNI 自從 JDK 1.1 發行版以來一直是 Java 平臺的一部分,並且在 JDK 1.2 發行版中得到了擴充套件。JDK 1.0 發行版包含一個早期的本機方法介面,但是未明確分隔本機程式碼和 Java 程式碼。在這個介面中,本機程式碼可以直接進入 JVM 結構,因此無法跨 JVM 實現、平臺或者甚至各種 JDK 版本進行移植。使用 JDK 1.0 模型升級含有大量本機程式碼的應用程式,以及開發能支援多個 JVM 實現的本機程式碼的開銷是極高的。

JDK 1.1 中引入的 JNI 支援:

● 版本獨立性

● 平臺獨立性

● VM 獨立性

● 開發第三方類庫

有一個有趣的地方值得注意,一些較年輕的語言(如 PHP)在它們的本機程式碼支援方面仍然在努力克服這些問題。

● 與舊有程式碼整合,避免重新編寫。

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

● 最好與使用 C/C++ 編寫的程式碼整合,以充分發掘效能或其他與環境相關的系統特性。

● 解決需要非 Java 程式碼的特殊情況。舉例來說,核心類庫的實現可能需要跨包呼叫或者需要繞過其他 Java 安全性檢查。

JNI 允許您完成這些任務。它明確分開了 Java 程式碼與本機程式碼(C/C++)的執行,定義了一個清晰的 API 在這兩者之間進行通訊。從很大程度上說,它避免了本機程式碼對 JVM 的直接記憶體引用,從而確保本機程式碼只需編寫一次,並且可以跨不同的 JVM 實現或版本執行。

藉助 JNI,本機程式碼可以隨意與 Java 物件互動,獲取和設計欄位值,以及呼叫方法,而不會像 Java 程式碼中的相同功能那樣受到諸多限制。這種自由是一把雙刃劍:它犧牲 Java 程式碼的安全性,換取了完成上述所列任務的能力。在您的應用程式中使用 JNI 提供了強大的、對機器資源(記憶體、I/O 等)的低階訪問,因此您不會像普通 Java 開發人員那樣受到安全網的保護。JNI 的靈活性和強大性帶來了一些程式設計實踐上的風險,比如導致效能較差、出現 bug 甚至程式崩潰。您必須格外留意應用程式中的程式碼,並使用良好的實踐來保障應用程式的總體完整性。

本文介紹 JNI 使用者最常遇到的 10 大編碼和設計錯誤。其目標是幫助您認識到並避免它們,以便您可以編寫安全、高效、效能出眾的 JNI 程式碼。本文還將介紹一些用於在新程式碼或已有程式碼中查詢這些問題的工具和技巧,並展示如何有效地應用它們。

JNI 程式設計缺陷可以分為兩類:

● 效能:程式碼能執行所設計的功能,但執行緩慢或者以某種形式拖慢整個程式。

● 正確性:程式碼有時能正常執行,但不能可靠地提供所需的功能;最壞的情況是造成程式崩潰或掛起。

效能缺陷

程式設計師在使用 JNI 時的 5 大效能缺陷如下:

● 不快取方法 ID、欄位 ID 和類

● 觸發陣列副本

● 回訪(Reaching back)而不是傳遞引數

● 錯誤認定本機程式碼與 Java 程式碼之間的界限

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

不快取方法 ID、欄位 ID 和類

要訪問 Java 物件的欄位並呼叫它們的方法,本機程式碼必須呼叫 FindClass()GetFieldID()GetMethodId() 和GetStaticMethodID()。對於 GetFieldID()GetMethodID() 和 GetStaticMethodID(),為特定類返回的 ID 不會在 JVM 程式的生存期內發生變化。但是,獲取欄位或方法的呼叫有時會需要在 JVM 中完成大量工作,因為欄位和方法可能是從超類中繼承而來的,這會讓 JVM 向上遍歷類層次結構來找到它們。由於 ID 對於特定類是相同的,因此您只需要查詢一次,然後便可重複使用。同樣,查詢類物件的開銷也很大,因此也應該快取它們。

舉例來說,清單 1 展示了呼叫靜態方法所需的 JNI 程式碼:

清單 1. 使用 JNI 呼叫靜態方法

當我們每次希望呼叫方法時查詢類和方法 ID 都會產生六個本機呼叫,而不是第一次快取類和方法 ID 時需要的兩個呼叫。

快取會對您應用程式的執行時造成顯著的影響。考慮下面兩個版本的方法,它們的作用是相同的。清單 2 使用了快取的欄位 ID:

清單 2. 使用快取的欄位 ID

效能技巧 #1

查詢並全域性快取常用的類、欄位 ID 和方法 ID。

清單 3 沒有使用快取的欄位 ID:

清單 3. 未快取欄位 ID

清單 2 用 3,572 ms 執行了 10,000,000 次。清單 3 用了 86,217 ms — 多花了 24 倍的時間。

觸發陣列副本

JNI 在 Java 程式碼和本機程式碼之間提供了一個乾淨的介面。為了維持這種分離,陣列將作為不透明的控制程式碼傳遞,並且本機程式碼必須回撥 JVM 以便使用 set 和 get 呼叫運算元組元素。Java 規範讓 JVM 實現決定讓這些呼叫提供對陣列的直接訪問,還是返回一個陣列副本。舉例來說,當陣列經過優化而不需要連續儲存時,JVM 可以返回一個副本。(參見 參考資料 獲取關於 JVM 的資訊)。

隨後,這些呼叫可以複製被操作的元素。舉例來說,如果您對含有 1,000 個元素的陣列呼叫 GetLongArrayElements(),則會造成至少分配或複製 8,000 位元組的資料(每個 long 1,000 元素 * 8 位元組)。當您隨後使用 ReleaseLongArrayElements() 更新陣列的內容時,需要另外複製 8,000 位元組的資料來更新陣列。即使您使用較新的 GetPrimitiveArrayCritical(),規範仍然准許 JVM 建立完整陣列的副本。

效能技巧 #2

獲取和更新僅本機程式碼需要的陣列部分。在只要陣列的一部分時通過適當的 API 呼叫來避免複製整個陣列。

GetTypeArrayRegion() 和 SetTypeArrayRegion() 方法允許您獲取和更新陣列的一部分,而不是整個陣列。通過使用這些方法訪問較大的陣列,您可以確保只複製本機程式碼將要實際使用的陣列部分。

舉例來說,考慮相同方法的兩個版本,如清單 4 所示:

清單 4. 相同方法的兩個版本

第一個版本可以生成兩個完整的陣列副本,而第二個版本則完全沒有複製陣列。當陣列大小為 1,000 位元組時,執行第一個方法 10,000,000 次用了 12,055 ms;而第二個版本僅用了 1,421 ms。第一個版本多花了 8.5 倍的時間!

效能技巧 #3

在單個 API 呼叫中儘可能多地獲取或更新陣列內容。如果可以一次較多地獲取和更新陣列內容,則不要逐個迭代陣列中的元素。

另一方面,如果您最終要獲取陣列中的所有元素,則使用GetTypeArrayRegion() 逐個獲取陣列中的元素是得不償失的。要獲取最佳的效能,應該確保以儘可能大的塊的來獲取和更新陣列元素。如果您要迭代一個陣列中的所有元素,則 清單 4 中這兩個 getElement() 方法都不適用。比較好的方法是在一個呼叫中獲取大小合理的陣列部分,然後再迭代所有這些元素,重複操作直到覆蓋整個陣列。

回訪而不是傳遞引數

在呼叫某個方法時,您經常會在傳遞一個有多個欄位的物件以及單獨傳遞欄位之間做出選擇。在物件導向設計中,傳遞物件通常能提供較好的封裝,因為物件欄位的變化不需要改變方法簽名。但是,對於 JNI 來說,本機程式碼必須通過一個或多個 JNI 呼叫返回到 JVM 以獲取需要的各個欄位的值。這些額外的呼叫會帶來額外的開銷,因為從本機程式碼過渡到 Java 程式碼要比普通方法呼叫開銷更大。因此,對於 JNI 來說,本機程式碼從傳遞進來的物件中訪問大量單獨欄位時會導致效能降低。

考慮清單 5 中的兩個方法,第二個方法假定我們快取了欄位 ID:

清單 5. 兩個方法版本

效能技巧 #4

如果可能,將各引數傳遞給 JNI 本機程式碼,以便本機程式碼回撥 JVM 獲取所需的資料。

sumValues2() 方法需要 6 個 JNI 回撥,並且執行 10,000,000 次需要 3,572 ms。其速度比 sumValues() 慢 6 倍,後者只需要 596 ms。通過傳遞 JNI 方法所需的資料,sumValues() 避免了大量的 JNI 開銷。

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

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

效能技巧 #5

定義 Java 程式碼與本機程式碼之間的界限,最大限度地減少兩者之間的互相呼叫。

因此,在設計 Java 程式碼與本機程式碼之間的界限時應該最大限度地減少兩者之間的相互呼叫。消除不必要的越界呼叫,並且應該竭力在本機程式碼中彌補越界呼叫造成的成本損失。最大限度地減少越界呼叫的一個關鍵因素是確保資料處於 Java/本機界限的正確一側。如果資料未在正確的一側,則另一側訪問資料的需求則會持續發起越界呼叫。

舉例來說,如果我們希望使用 JNI 為某個串列埠提供介面,則可以構造兩種不同的介面。第一個版本如清單 6 所示:

清單 6. 到串列埠的介面:版本 1

在 清單 6 中,串列埠的所有配置資料都儲存在由 initializeSerialPort() 方法返回的 Java 物件中,並且將 Java 程式碼完全控制對硬體中各資料位的設定。清單 6 所示版本的一些問題會造成其效能差於清單 7 中的版本:

清單 7. 到串列埠的介面:版本 2

效能技巧 #6

構造應用程式的資料,使它位於界限的正確的側,並且可以由使用它的程式碼訪問,而不需要大量跨界呼叫。

最顯著的一個問題就是,清單 6 中的介面在設定或檢索每個位,以及從串列埠讀取位元組或者向串列埠寫入位元組都需要一個 JNI 呼叫。這會導致讀取或寫入的每個位元組的 JNI 呼叫變成原來的 9 倍。第二個問題是,清單 6 將串列埠的配置資訊儲存在 Java/本機界限的錯誤一側的某個 Java 物件上。我們僅在本機側需要此配置資料;將它儲存在 Java 側會導致本機程式碼向 Java 程式碼發起大量回撥以獲取/設定此配置資訊。清單 7 將配置資訊儲存在一個本機結構中(比如,一個struct),並向 Java 程式碼返回了一個不透明的控制程式碼,該控制程式碼可以在後續呼叫中返回。這意味著,當本機程式碼正在執行時,它可以直接訪問該結構,而不需要回撥 Java 程式碼獲取串列埠硬體地址或下一個可用的緩衝區等資訊。因此,使用 清單 7 的實現的效能將大大改善。

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

JNI 函式返回的任何物件都會建立本地引用。舉例來說,當您呼叫 GetObjectArrayElement() 時,將返回對陣列中物件的本地引用。考慮清單 8 中的程式碼在執行一個很大的陣列時會使用多少本地引用:

清單 8. 建立本地引用

每次呼叫 GetObjectArrayElement() 時都會為元素建立一個本地引用,並且直到本機程式碼執行完成時才會釋放。陣列越大,所建立的本地引用就越多。

效能技巧 #7

當本機程式碼造成建立大量本地引用時,在各引用不再需要時刪除它們。

這些本地引用會在本機方法終止時自動釋放。JNI 規範要求各本機程式碼至少能建立 16 個本地引用。雖然這對許多方法來說都已經足夠了,但一些方法在其生存期中卻需要更多的本地引用。對於這種情況,您應該刪除不再需要的引用,方法是使用 JNI DeleteLocalRef() 呼叫,或者通知 JVM 您將使用更多的本地引用。

清單 9 向 清單 8 中的示例新增了一個 DeleteLocalRef() 呼叫,用於通知 JVM 本地引用已不再需要,以及將可同時存在的本地引用的數量限制為一個合理的數值,而與陣列的大小無關:

清單 9. 新增 DeleteLocalRef() 

效能技巧 #8

如果某本機程式碼將同時存在大量本地引用,則呼叫 JNIEnsureLocalCapacity() 方法通知 JVM 並允許它優化對本地引用的處理。

您可以呼叫 JNI EnsureLocalCapacity() 方法來通知 JVM 您將使用超過 16 個本地引用。這將允許 JVM 優化對該本機程式碼的本地引用的處理。如果無法建立所需的本地引用,或者 JVM 採用的本地引用管理方法與所使用的本地引用數量之間不匹配造成了效能低下,則未成功通知 JVM 會導致 FatalError

正確性缺陷

5 大 JNI 正確性缺陷包括:

● 使用錯誤的 JNIEnv

● 未檢測異常

● 未檢測返回值

● 未正確使用陣列方法

● 未正確使用全域性引用

使用錯誤的 JNIEnv

執行本機程式碼的執行緒使用 JNIEnv 發起 JNI 方法呼叫。但是,JNIEnv 並不是僅僅用於分派所請求的方法。JNI 規範規定每個 JNIEnv 對於執行緒來說都是本地的。JVM 可以依賴於這一假設,將額外的執行緒本地資訊儲存在 JNIEnv 中。一個執行緒使用另一個執行緒中的 JNIEnv會導致一些小 bug 和難以除錯的崩潰問題。

正確性技巧 #1

僅在相關的單一執行緒中使用 JNIEnv

執行緒可以呼叫通過 JavaVM 物件使用 JNI 呼叫介面的 GetEnv() 來獲取JNIEnvJavaVM 物件本身可以通過使用 JNIEnv 方法呼叫 JNIGetJavaVM() 來獲取,並且可以被快取以及跨執行緒共享。快取 JavaVM 物件的副本將允許任何能訪問快取物件的執行緒在必要時獲取對它自己的JNIEnv 訪問。要實現最優效能,執行緒應該繞過 JNIEnv,因為查詢它有時會需要大量的工作。

未檢測異常

本機能呼叫的許多 JNI 方法都會引起與執行執行緒相關的異常。當 Java 程式碼執行時,這些異常會造成執行流程發生變化,這樣便會自動呼叫異常處理程式碼。當某個本機方法呼叫某個 JNI 方法時會出現異常,但檢測異常並採用適當措施的工作將由本機來完成。一個常見的 JNI 缺陷是呼叫 JNI 方法而未在呼叫完成後測試異常。這會造成程式碼有大量漏洞以及程式崩潰。

舉例來說,考慮呼叫 GetFieldID() 的程式碼,如果無法找到所請求的欄位,則會出現 NoSuchFieldError。如果本機程式碼繼續執行而未檢測異常,並使用它認為應該返回的欄位 ID,則會造成程式崩潰。舉例來說,如果 Java 類經過修改,導致 charField 欄位不再存在,則清單 10 中的程式碼可能會造成程式崩潰 — 而不是丟擲一個 NoSuchFieldError

清單 10. 未能檢測異常

正確性技巧 #2

在發起可能會導致異常的 JNI 呼叫後始終檢測異常。

新增異常檢測程式碼要比在事後嘗試除錯崩潰簡單很多。經常,您只需要檢測是否出現了某個異常,如果是則立即返回 Java 程式碼以便丟擲異常。然後,使用常規的 Java 異常處理流程處理它或者顯示它。舉例來說,清單 11 將檢測異常:

清單 11. 檢測異常

不檢測和清除異常會導致出現意外行為。您可以確定以下程式碼的問題嗎?

問題在於,儘管程式碼處理了初始 GetFieldID() 未返回欄位 ID 的情況,但它並未清除 此呼叫將設定的異常。因此,本機返回的結果會造成立即丟擲一個異常。

未檢測返回值

許多 JNI 方法都通過返回值來指示呼叫成功與否。與未檢測異常相似,這也存在一個缺陷,即程式碼未檢測返回值卻假定呼叫成功而繼續執行。對於大多數 JNI 方法來說,它們都設定了返回值和異常狀態,這樣應用程式更可以通過檢測異常狀態或返回值來判斷方法執行正常與否。

正確性技巧 #3

始終檢測 JNI 方法的返回值,幷包括用於處理錯誤的程式碼路徑。

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

問題在於,如果未發現 HelloWorld 類,或者如果 main() 不存在,則本機將造成程式崩潰。

未正確使用陣列方法

GetXXXArrayElements() 和 ReleaseXXXArrayElements() 方法允許您請求任何元素。同樣,GetPrimitiveArrayCritical()ReleasePrimitiveArrayCritical()

GetStringCritical() 和 ReleaseStringCritical() 允許您請求陣列元素或字串位元組,以最大限度降低直接指向陣列或字串的可能性。這些方法的使用存在兩個常見的缺陷。其一,忘記在 ReleaseXXX() 方法呼叫中提供更改。即便使用 Critical 版本,也無法保證您能獲得對陣列或字串的直接引用。一些 JVM 始終返回一個副本,並且在這些 JVM 中,如果您在 ReleaseXXX() 呼叫中指定了 JNI_ABORT,或者忘記呼叫了 ReleaseXXX(),則對陣列的更改不會被複制回去。

舉例來說,考慮以下程式碼:

正確性技巧 #4

不要忘記為每個 GetXXX() 使用模式 0(複製回去並釋放記憶體)呼叫 ReleaseXXX()

在提供直接指向陣列的指標的 JVM 上,該陣列將被更新;但是,在返回副本的 JVM 上則不是如此。這會造成您的程式碼在一些 JVM 上能夠正常執行,而在其他 JVM 上卻會出錯。您應該始終始終包括一個釋放(release)呼叫,如清單 12 所示:

清單 12. 包括一個釋放呼叫

第二個缺陷是不注重規範對在 GetXXXCritical() 和 ReleaseXXXCritical() 之間執行的程式碼施加的限制。本機可能不會在這些方法之間發起任何呼叫,並且可能不會由於任何原因而阻塞。未重視這些限制會造成應用程式或 JVM 中出現間斷性死鎖。

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

正確性技巧 #5

確保程式碼不會在 GetXXXCritical() 和ReleaseXXXCritical() 呼叫之間發起任何 JNI 呼叫或由於任何原因出現阻塞。

但是,我們需要驗證在呼叫 processBufferHelper() 時可以執行的所有程式碼都沒有違反任何限制。這些限制適用於在 Get 和 Release 呼叫之間執行的所有程式碼,無論它是不是本機的一部分。

未正確使用全域性引用

本機可以建立一些全域性引用,以保證物件在不再需要時才會被垃圾收集器回收。常見的缺陷包括忘記刪除已建立的全域性引用,或者完全失去對它們的跟蹤。考慮一個本機建立了全域性引用,但是未刪除它或將它儲存在某處:

正確性技巧 #6

始終跟蹤全域性引用,並確保不再需要物件時刪除它們。

建立全域性引用時,JVM 會將它新增到一個禁止垃圾收集的物件列表中。當本機返回時,它不僅會釋放全域性引用,應用程式還無法獲取引用以便稍後釋放它 — 因此,物件將會始終存在。不釋放全域性引用會造成各種問題,不僅因為它們會保持物件本身為活動狀態,還因為它們會將通過該物件能接觸到的所有物件都保持為活動狀態。在某些情況下,這會顯著加劇記憶體洩漏。

避免常見缺陷

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

表 1. 確定 JNI 程式設計缺陷的清單

未快取 觸發陣列副本 錯誤界限 過多回訪 使用大量本地引用 使用錯誤的 JNIEnv 未檢測異常 未檢測返回值 未正確使用陣列 未正確使用全域性引用
規範驗證 X X X
方法跟蹤 X X X X X X X
轉儲 X
-verbose:jni X
程式碼審查 X X X X X X X X X X

您可以在開發週期的早期確定許多常見缺陷,方法如下:

● 根據規範驗證新程式碼

● 分析方法跟蹤

● 使用 -verbose:jni 選項

● 生成轉儲

● 執行程式碼審查

根據 JNI 規範驗證新程式碼

維持規範的限制列表並審查本機與列表的遵從性是一個很好的實踐,這可以通過手動或自動程式碼分析來完成。確保遵從性的工作可能會比除錯由於違背限制而出現的細小和間斷性故障輕鬆很多。下面提供了一個專門針對新開發程式碼(或對您來說是新的)的規範順從性檢查列表:

● 驗證 JNIEnv 僅與與之相關的執行緒使用。

● 確認未在 GetXXXCritical() 的 ReleaseXXXCritical() 部分呼叫 JNI 方法。

● 對於進入關鍵部分的方法,驗證該方法未在釋放前返回。

● 驗證在所有可能引起異常的 JNI 呼叫之前都檢測了異常。

● 確保所有 Get/Release 呼叫在各 JNI 方法中都是相匹配的。

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

● 傳遞給 JNI 方法的引數屬於正確的型別。

● JNI 程式碼未讀取超過陣列結束部分之外的內容。

● 傳遞給 JNI 方法的指標都是有效的。

JNI 檢測報告的所有結論並不一定都是程式碼中的錯誤。它們還包括一些針對程式碼的建議,您應該仔細閱讀它們以確保程式碼功能正常。

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

使用 IBM JVM 的 -Xcheck:jni 選項作為標準開發流程的一部分可以幫助您更加輕鬆地找出程式碼錯誤。特別是,它可以幫助您確定在錯誤執行緒中使用 JNIEnv 以及未正確使用關鍵區域的缺陷的根源。

最新的 Sun JVM 提供了一個類似的 -Xcheck:jni 選項。它的工作原理不同於 IBM 版本,並且提供了不同的資訊,但是它們的作用是相同的。它會在發現未符合規範的程式碼時發出警告,並且可以幫助您確定常見的 JNI 缺陷。

分析方法跟蹤

生成對已呼叫本機方法以及這些本機方法發起的 JNI 回撥的跟蹤,這對確定大量常見缺陷的根源是非常有用的。可確定的問題包括:

● 大量 GetFieldID() 和 GetMethodID() 呼叫 — 特別是,如果這些呼叫針對相同的欄位和方法 — 表示欄位和方法未被快取。

● GetTypeArrayElements() 呼叫例項(而非 GetTypeArrayRegion())有時表示存在不必要的複製。

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

● 每個本機函式呼叫後面都緊接著大量 GetFieldID() 呼叫,這種模式表示並未傳遞所需的引數,而是強制本機回訪完成工作所需的資料。

● 呼叫可能丟擲異常的 JNI 方法之後缺少對 ExceptionOccurred() 或 ExceptionCheck() 的呼叫表示本機未正確檢測異常。

● GetXXX() 和 ReleaseXXX() 方法呼叫的數量不匹配表示缺少釋放操作。

● 在 GetXXXCritical() 和 ReleaseXXXCritical() 呼叫之間呼叫 JNI 方法表示未遵循規範施加的限制。

● 如果呼叫 GetXXXCritical() 和 ReleaseXXXCritical() 之間相隔的時間較長,則表示未遵循 “不要阻塞呼叫” 規範所施加的限制。

● NewGlobalRef() 和 DeleteGlobalRef() 呼叫之間出現嚴重失衡表示釋放不再需要的引用時出現故障。

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

IBM JVM 實現提供了許多用於生成跟蹤資訊的方法。第一種方法是使用 -Xcheck:jni:trace 選項。這將生成對已呼叫的本機方法以及它們發起的 JNI 回撥的跟蹤。清單 13 顯示某個跟蹤的摘錄(為便於閱讀,隔開了某些行):

清單 13. IBM JVM 實現所生成的方法跟蹤

清單 13 中的跟蹤摘錄顯示了已呼叫的本機方法(比如 AccessController.initializeInternal()V)以及本機方法發起的 JNI 回撥。

使用 -verbose:jni 選項

Sun 和 IBM JVM 還提供了一個 -verbose:jni 選項。對於 IBM JVM 而言,開啟此選項將提供關於當前 JNI 回撥的資訊。清單 14 顯示了一個示例:

清單 14. 使用 IBM JVM 的 -verbose:jni 列出 JNI 回撥

對於 Sun JVM 而言,開啟 -verbose:jni 選項不會提供關於當前呼叫的資訊,但它會提供關於所使用的本機方法的額外資訊。清單 15 顯示了一個示例:

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

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

雖然 -verbose:jni 和 -Xcheck:jni:trace 選項可幫助您方便地獲取所需的資訊,但手動審查此資訊是一項艱鉅的任務。一個不錯的提議是,建立一些指令碼或實用工具來處理由 JVM 生成的跟蹤檔案,並檢視 警告

生成轉儲

執行中的 Java 程式生成的轉儲包含大量關於 JVM 狀態的資訊。對於許多 JVM 來說,它們包括關於全域性引用的資訊。舉例來說,最新的 Sun JVM 在轉儲資訊中包括這樣一行:

通過生成前後轉儲,您可以確定是否建立了任何未正常釋放的全域性引用。

您可以在 UNIX® 環境中通過對 java 程式發起 kill -3 或 kill -QUIT 來請求轉儲。在 Windows® 上,使用 Ctrl+Break 組合鍵。

對於 IBM JVM,使用以下步驟獲取關於全域性引用的資訊:

1. 將 -Xdump:system:events=user 新增到命令列。這樣,當您在 UNIX 系統上呼叫 kill -3 或者在 Windows 上按下 Ctrl+Break 時,JVM 便會生成轉儲。

2.程式在執行中時會生成後續轉儲。

3.執行 jextract -nozip core.XXX output.xml,這將會將轉儲資訊提取到可讀格式的 output.xml 中。

4.查詢 output.xml 中的 JNIGlobalReference 條目,它提供關於當前全域性引用的資訊,如清單 16 所示:

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

通過檢視後續 Java 轉儲中報告的數值,您可以確定全域性引用是否出現的洩漏。

參見 參考資料 獲取關於使用轉儲檔案以及 IBM JVM 的 jextract 的更多資訊。

執行程式碼審查

程式碼審查經常可用於確定常見缺陷,並且可以在各種級別上完成。繼承新程式碼時,快速掃描可以發現各種問題,從而避免稍後花費更多時間進行除錯。在某些情況下,審查是確定缺陷例項(比如未檢查返回值)的唯一方法。舉例來說,此程式碼的問題可能可以通過程式碼審查輕鬆確定,但卻很難通過除錯來發現:

程式碼審查可能會發現第一個方法未正確快取欄位 ID,儘管重複使用了相同的 ID,並且第二個方法所使用的 JNIEnv 並不在應該在的執行緒上。

結束語

現在,您已經瞭解了 10 大 JNI 程式設計缺陷,以及一些用於在已有或新程式碼中確定它們的良好實踐。堅持應用這些實踐有助於提高 JNI 程式碼的正確率,並且您的應用程式可以實現所需的效能水平。

有效整合已有程式碼資源的能力對於物件導向架構(SOA)和基於雲的計算這兩種技術的成功至關重要。JNI 是一項非常重要的技術,用於將非 Java 舊有程式碼和元件整合到基於 Java 的平臺中,充當 SOA 或基於雲的系統的基本元素。正確使用 JNI 可以加速將這些元件轉變為服務的過程,並允許您從現有投資中獲得最大優勢。

 

相關文章