在 JNI 編程中避免內存泄漏

轉自:http://www.ibm.com/developerworks/cn/java/j-lo-jnileak/index.html?ca=drs-

JNI 編程簡介

JNI,Java Native Interface,是 native code 的編程接口。JNI 使 Java 代碼程序可以與 native code 交互——在 Java 程序中調用 native code;在 native code 中嵌入 Java 虛擬機調用 Java 的代碼。

JNI 編程在軟件開發中運用廣泛,其優勢可以歸結爲以下幾點:

  1. 利用 native code 的平臺相關性,在平臺相關的編程中彰顯優勢。
  2. 對 native code 的代碼重用。
  3. native code 底層操作,更加高效。

然而任何事物都具有兩面性,JNI 編程也同樣如此。程序員在使用 JNI 時應當認識到 JNI 編程中如下的幾點弊端,揚長避短,纔可以寫出更加完善、高性能的代碼:

  1. 從 Java 環境到 native code 的上下文切換耗時、低效。
  2. JNI 編程,如果操作不當,可能引起 Java 虛擬機的崩潰。
  3. JNI 編程,如果操作不當,可能引起內存泄漏。

JAVA 中的內存泄漏

JAVA 編程中的內存泄漏,從泄漏的內存位置角度可以分爲兩種:JVM 中 Java Heap 的內存泄漏;JVM 內存中 native memory 的內存泄漏。

Java Heap 的內存泄漏

Java 對象存儲在 JVM 進程空間中的 Java Heap 中,Java Heap 可以在 JVM 運行過程中動態變化。如果 Java 對象越來越多,佔據 Java Heap 的空間也越來越大,JVM 會在運行時擴充 Java Heap 的容量。如果 Java Heap 容量擴充到上限,並且在 GC 後仍然沒有足夠空間分配新的 Java 對象,便會拋出 out of memory 異常,導致 JVM 進程崩潰。

Java Heap 中 out of memory 異常的出現有兩種原因——①程序過於龐大,致使過多 Java 對象的同時存在;②程序編寫的錯誤導致 Java Heap 內存泄漏。

多種原因可能導致 Java Heap 內存泄漏。JNI 編程錯誤也可能導致 Java Heap 的內存泄漏。

JVM 中 native memory 的內存泄漏

從操作系統角度看,JVM 在運行時和其它進程沒有本質區別。在系統級別上,它們具有同樣的調度機制,同樣的內存分配方式,同樣的內存格局。

JVM 進程空間中,Java Heap 以外的內存空間稱爲 JVM 的 native memory。進程的很多資源都是存儲在 JVM 的 native memory 中,例如載入的代碼映像,線程的堆棧,線程的管理控制塊,JVM 的靜態數據、全局數據等等。也包括 JNI 程序中 native code 分配到的資源。

在 JVM 運行中,多數進程資源從 native memory 中動態分配。當越來越多的資源在 native memory 中分配,佔據越來越多 native memory 空間並且達到 native memory 上限時,JVM 會拋出異常,使 JVM 進程異常退出。而此時 Java Heap 往往還沒有達到上限。

多種原因可能導致 JVM 的 native memory 內存泄漏。例如 JVM 在運行中過多的線程被創建,並且在同時運行。JVM 爲線程分配的資源就可能耗盡 native memory 的容量。

JNI 編程錯誤也可能導致 native memory 的內存泄漏。對這個話題的討論是本文的重點。

JNI 編程中明顯的內存泄漏

JNI 編程實現了 native code 和 Java 程序的交互,因此 JNI 代碼編程既遵循 native code 編程語言的編程規則,同時也遵守 JNI 編程的文檔規範。在內存管理方面,native code 編程語言本身的內存管理機制依然要遵循,同時也要考慮 JNI 編程的內存管理。

本章簡單概括 JNI 編程中顯而易見的內存泄漏。從 native code 編程語言自身的內存管理,和 JNI 規範附加的內存管理兩方面進行闡述。

Native Code 本身的內存泄漏

JNI 編程首先是一門具體的編程語言,或者 C 語言,或者 C++,或者彙編,或者其它 native 的編程語言。每門編程語言環境都實現了自身的內存管理機制。因此,JNI 程序開發者要遵循 native 語言本身的內存管理機制,避免造成內存泄漏。以 C 語言爲例,當用 malloc() 在進程堆中動態分配內存時,JNI 程序在使用完後,應當調用 free() 將內存釋放。總之,所有在 native 語言編程中應當注意的內存泄漏規則,在 JNI 編程中依然適應。

Native 語言本身引入的內存泄漏會造成 native memory 的內存,嚴重情況下會造成 native memory 的 out of memory。

Global Reference 引入的內存泄漏

JNI 編程還要同時遵循 JNI 的規範標準,JVM 附加了 JNI 編程特有的內存管理機制。

JNI 中的 Local Reference 只在 native method 執行時存在,當 native method 執行完後自動失效。這種自動失效,使得對 Local Reference 的使用相對簡單,native method 執行完後,它們所引用的 Java 對象的 reference count 會相應減 1。不會造成 Java Heap 中 Java 對象的內存泄漏。

而 Global Reference 對 Java 對象的引用一直有效,因此它們引用的 Java 對象會一直存在 Java Heap 中。程序員在使用 Global Reference 時,需要仔細維護對 Global Reference 的使用。如果一定要使用 Global Reference,務必確保在不用的時候刪除。就像在 C 語言中,調用 malloc() 動態分配一塊內存之後,調用 free() 釋放一樣。否則,Global Reference 引用的 Java 對象將永遠停留在 Java Heap 中,造成 Java Heap 的內存泄漏。

JNI 編程中潛在的內存泄漏——對 LocalReference 的深入理解

Local Reference 在 native method 執行完成後,會自動被釋放,似乎不會造成任何的內存泄漏。但這是錯誤的。對 Local Reference 的理解不夠,會造成潛在的內存泄漏。

本章重點闡述 Local Reference 使用不當可能引發的內存泄漏。引入兩個錯誤實例,也是 JNI 程序員容易忽視的錯誤;在此基礎上介紹 Local Reference 表,對比 native method 中的局部變量和 JNI Local Reference 的不同,使讀者深入理解 JNI Local Reference 的實質;最後爲 JNI 程序員提出應該如何正確合理使用 JNI Local Reference,以避免內存泄漏。

錯誤實例 1

在某些情況下,我們可能需要在 native method 裏面創建大量的 JNI Local Reference。這樣可能導致 native memory 的內存泄漏,如果在 native method 返回之前 native memory 已經被用光,就會導致 native memory 的 out of memory。

在代碼清單 1 裏,我們循環執行 count 次,JNI function NewStringUTF() 在每次循環中從 Java Heap 中創建一個 String 對象,str 是 Java Heap 傳給 JNI native method 的 Local Reference,每次循環中新創建的 String 對象覆蓋上次循環中 str 的內容。str 似乎一直在引用到一個 String 對象。整個運行過程中,我們看似只創建一個 Local Reference。

執行代碼清單 1 的程序,第一部分爲 Java 代碼,nativeMethod(int i) 中,輸入參數設定循環的次數。第二部分爲 JNI 代碼,用 C 語言實現了 nativeMethod(int i)。


清單 1. Local Reference 引發內存泄漏

Java 代碼部分
 class TestLocalReference { 
 private native void nativeMethod(int i); 
 public static void main(String args[]) { 
         TestLocalReference c = new TestLocalReference(); 
         //call the jni native method 
         c.nativeMethod(1000000); 
 }  
 static { 
 //load the jni library 
 System.loadLibrary("StaticMethodCall"); 
 } 
 } 


 JNI 代碼,nativeMethod(int i) 的 C 語言實現
 #include<stdio.h> 
 #include<jni.h> 
 #include"TestLocalReference.h"
 JNIEXPORT void JNICALL Java_TestLocalReference_nativeMethod 
 (JNIEnv * env, jobject obj, jint count) 
 { 
 jint i = 0; 
 jstring str; 


 for(; i<count; i++) 
         str = (*env)->NewStringUTF(env, "0"); 
 } 
運行結果
 JVMCI161: FATAL ERROR in native method: Out of memory when expanding 
 local ref table beyond capacity 
 at TestLocalReference.nativeMethod(Native Method) 
 at TestLocalReference.main(TestLocalReference.java:9)


運行結果證明,JVM 運行異常終止,原因是創建了過多的 Local Reference,從而導致 out of memory。實際上,nativeMethod 在運行中創建了越來越多的 JNI Local Reference,而不是看似的始終只有一個。過多的 Local Reference,導致了 JNI 內部的 JNI Local Reference 表內存溢出。

錯誤實例 2

實例 2 是實例 1 的變種,Java 代碼未作修改,但是 nativeMethod(int i) 的 C 語言實現稍作修改。在 JNI 的 native method 中實現的 utility 函數中創建 Java 的 String 對象。utility 函數只建立一個 String 對象,返回給調用函數,但是 utility 函數對調用者的使用情況是未知的,每個函數都可能調用它,並且同一函數可能調用它多次。在實例 2 中,nativeMethod 在循環中調用 count 次,utility 函數在創建一個 String 對象後即返回,並且會有一個退棧過程,似乎所創建的 Local Reference 會在退棧時被刪除掉,所以應該不會有很多 Local Reference 被創建。實際運行結果並非如此。


清單 2. Local Reference 引發內存泄漏
Java 代碼部分參考實例 1,未做任何修改。


 
JNI 代碼,nativeMethod(int i) 的 C 語言實現
 #include<stdio.h> 
 #include<jni.h> 
 #include"TestLocalReference.h"
 jstring CreateStringUTF(JNIEnv * env) 
 { 
 return (*env)->NewStringUTF(env, "0"); 
 } 
 JNIEXPORT void JNICALL Java_TestLocalReference_nativeMethod 
 (JNIEnv * env, jobject obj, jint count) 
 { 
 jint i = 0; 
 for(; i<count; i++) 
 { 
         str = CreateStringUTF(env); 
 } 
 } 
運行結果
 JVMCI161: FATAL ERROR in native method: Out of memory when expanding local ref 
 table beyond  capacity 
 at TestLocalReference.nativeMethod(Native Method) 
 at TestLocalReference.main(TestLocalReference.java:9) 


運行結果證明,實例 2 的結果與實例 1 的完全相同。過多的 Local Reference 被創建,仍然導致了 JNI 內部的 JNI Local Reference 表內存溢出。實際上,在 utility 函數 CreateStringUTF(JNIEnv * env)

執行完成後的退棧過程中,創建的 Local Reference 並沒有像 native code 中的局部變量那樣被刪除,而是繼續在 Local Reference 表中存在,並且有效。Local Reference 和局部變量有着本質的區別。

Local Reference 深層解析

Java JNI 的文檔規範只描述了 JNI Local Reference 是什麼(存在的目的),以及應該怎麼使用 Local Reference(開放的接口規範)。但是對 Java 虛擬機中 JNI Local Reference 的實現並沒有約束,不同的 Java 虛擬機有不同的實現機制。這樣的好處是,不依賴於具體的 JVM 實現,有好的可移植性;並且開發簡單,規定了“應該怎麼做、怎麼用”。但是弊端是初級開發者往往看不到本質,“不知道爲什麼這樣做”。對 Local Reference 沒有深層的理解,就會在編程過程中無意識的犯錯。

Local Reference 和 Local Reference 表

理解 Local Reference 表的存在是理解 JNI Local Reference 的關鍵。

JNI Local Reference 的生命期是在 native method 的執行期(從 Java 程序切換到 native code 環境時開始創建,或者在 native method 執行時調用 JNI function 創建),在 native method 執行完畢切換回 Java 程序時,所有 JNI Local Reference 被刪除,生命期結束(調用 JNI function 可以提前結束其生命期)。

實際上,每當線程從 Java 環境切換到 native code 上下文時(J2N),JVM 會分配一塊內存,創建一個 Local Reference 表,這個表用來存放本次 native method 執行中創建的所有的 Local Reference。每當在 native code 中引用到一個 Java 對象時,JVM 就會在這個表中創建一個 Local Reference。比如,實例 1 中我們調用 NewStringUTF() 在 Java Heap 中創建一個 String 對象後,在 Local Reference 表中就會相應新增一個 Local Reference。


圖 1. Local Reference 表、Local Reference 和 Java 對象的關係
圖 1. Local Reference 表、Local Reference 和 Java 對象的關係 

圖 1 中:

⑴運行 native method 的線程的堆棧記錄着 Local Reference 表的內存位置(指針 p)。

⑵ Local Reference 表中存放 JNI Local Reference,實現 Local Reference 到 Java 對象的映射。

⑶ native method 代碼間接訪問 Java 對象(java obj1,java obj2)。通過指針 p 定位相應的 Local Reference 的位置,然後通過相應的 Local Reference 映射到 Java 對象。

⑷當 native method 引用一個 Java 對象時,會在 Local Reference 表中創建一個新 Local Reference。在 Local Reference 結構中寫入內容,實現 Local Reference 到 Java 對象的映射。

⑸ native method 調用 DeleteLocalRef() 釋放某個 JNI Local Reference 時,首先通過指針 p 定位相應的 Local Reference 在 Local Ref 表中的位置,然後從 Local Ref 表中刪除該 Local Reference,也就取消了對相應 Java 對象的引用(Ref count 減 1)。

⑹當越來越多的 Local Reference 被創建,這些 Local Reference 會在 Local Ref 表中佔據越來越多內存。當 Local Reference 太多以至於 Local Ref 表的空間被用光,JVM 會拋出異常,從而導致 JVM 的崩潰。

Local Ref 不是 native code 的局部變量

很多人會誤將 JNI 中的 Local Reference 理解爲 Native Code 的局部變量。這是錯誤的。

Native Code 的局部變量和 Local Reference 是完全不同的,區別可以總結爲:

⑴局部變量存儲在線程堆棧中,而 Local Reference 存儲在 Local Ref 表中。

⑵局部變量在函數退棧後被刪除,而 Local Reference 在調用 DeleteLocalRef() 後纔會從 Local Ref 表中刪除,並且失效,或者在整個 Native Method 執行結束後被刪除。

⑶可以在代碼中直接訪問局部變量,而 Local Reference 的內容無法在代碼中直接訪問,必須通過 JNI function 間接訪問。JNI function 實現了對 Local Reference 的間接訪問,JNI function 的內部實現依賴於具體 JVM。

代碼清單 1 中 str = (*env)->NewStringUTF(env, "0");

str 是 jstring 類型的局部變量。Local Ref 表中會新創建一個 Local Reference,引用到 NewStringUTF(env, "0") 在 Java Heap 中新建的 String 對象。如圖 2 所示:


圖 2. str 間接引用 string 對象
圖 2. str 間接引用 string 對象 

圖 2 中,str 是局部變量,在 native method 堆棧中。Local Ref3 是新創建的 Local Reference,在 Local Ref 表中,引用新創建的 String 對象。JNI 通過 str 和指針 p 間接定位 Local Ref3,但 p 和 Local Ref3 對 JNI 程序員不可見。

Local Reference 導致內存泄漏

在以上論述基礎上,我們通過分析錯誤實例 1 和實例 2,來分析 Local Reference 可能導致的內存泄漏,加深對 Local Reference 的深層理解。

分析錯誤實例 1:

局部變量 str 在每次循環中都被重新賦值,間接指向最新創建的 Local Reference,前面創建的 Local Reference 一直保留在 Local Ref 表中。

在實例 1 執行完第 i 次循環後,內存佈局如圖 3:


圖 3. 執行 i 次循環後的內存佈局
圖 3. 執行 i 次循環後的內存佈局 

繼續執行完第 i+1 次循環後,內存佈局發生變化,如圖 4:


圖 4. 執行 i+1 次循環後的內存佈局
圖 4. 執行 i+1 次循環後的內存佈局 

圖 4 中,局部變量 str 被賦新值,間接指向了 Local Ref i+1。在 native method 運行過程中,我們已經無法釋放 Local Ref i 佔用的內存,以及 Local Ref i 所引用的第 i 個 string 對象所佔據的 Java Heap 內存。所以,native memory 中 Local Ref i 被泄漏,Java Heap 中創建的第 i 個 string 對象被泄漏了。

也就是說在循環中,前面創建的所有 i 個 Local Reference 都泄漏了 native memory 的內存,創建的所有 i 個 string 對象都泄漏了 Java Heap 的內存。

直到 native memory 執行完畢,返回到 Java 程序時(N2J),這些泄漏的內存纔會被釋放,但是 Local Reference 表所分配到的內存往往很小,在很多情況下 N2J 之前可能已經引發嚴重內存泄漏,導致 Local Reference 表的內存耗盡,使 JVM 崩潰,例如錯誤實例 1。

分析錯誤實例 2:

實例 2 與實例 1 相似,雖然每次循環中調用工具函數 CreateStringUTF(env) 來創建對象,但是在 CreateStringUTF(env) 返回退棧過程中,只是局部變量被刪除,而每次調用創建的 Local Reference 仍然存在 Local Ref 表中,並且有效引用到每個新創建的 string 對象。str 局部變量在每次循環中被賦新值。

這樣的內存泄漏是潛在的,但是這樣的錯誤在 JNI 程序員編程過程中卻經常出現。通常情況,在觸發 out of memory 之前,native method 已經執行完畢,切換回 Java 環境,所有 Local Reference 被刪除,問題也就沒有顯露出來。但是某些情況下就會引發 out of memory,導致實例 1 和實例 2 中的 JVM 崩潰。

控制 Local Reference 生命期

因此,在 JNI 編程時,正確控制 JNI Local Reference 的生命期。如果需要創建過多的 Local Reference,那麼在對被引用的 Java 對象操作結束後,需要調用 JNI function(如 DeleteLocalRef()),及時將 JNI Local Reference 從 Local Ref 表中刪除,以避免潛在的內存泄漏。

總結

本文闡述了 JNI 編程可能引發的內存泄漏,JNI 編程既可能引發 Java Heap 的內存泄漏,也可能引發 native memory 的內存泄漏,嚴重的情況可能使 JVM 運行異常終止。JNI 軟件開發人員在編程中,應當考慮以下幾點,避免內存泄漏:

  • native code 本身的內存管理機制依然要遵循。
  • 使用 Global reference 時,當 native code 不再需要訪問 Global reference 時,應當調用 JNI 函數 DeleteGlobalRef() 刪除 Global reference 和它引用的 Java 對象。Global reference 管理不當會導致 Java Heap 的內存泄漏。
  • 透徹理解 Local reference,區分 Local reference 和 native code 的局部變量,避免混淆兩者所引起的 native memory 的內存泄漏。
  • 使用 Local reference 時,如果 Local reference 引用了大的 Java 對象,當不再需要訪問 Local reference 時,應當調用 JNI 函數 DeleteLocalRef() 刪除 Local reference,從而也斷開對 Java 對象的引用。這樣可以避免 Java Heap 的 out of memory。
  • 使用 Local reference 時,如果在 native method 執行期間會創建大量的 Local reference,當不再需要訪問 Local reference 時,應當調用 JNI 函數 DeleteLocalRef() 刪除 Local reference。Local reference 表空間有限,這樣可以避免 Local reference 表的內存溢出,避免 native memory 的 out of memory。
  • 嚴格遵循 Java JNI 規範書中的使用規則。

參考資料

學習

發佈了154 篇原創文章 · 獲贊 9 · 訪問量 108萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章