JNI,Java Native Interface,是 native code 的編程接口。JNI 使 Java 代碼程序可以與 native code 交互——在 Java 程序中調(diào)用 native code;在 native code 中嵌入 Java 虛擬機(jī)調(diào)用 Java 的代碼。 JNI 編程在軟件開發(fā)中運(yùn)用廣泛,其優(yōu)勢可以歸結(jié)為以下幾點(diǎn):
然而任何事物都具有兩面性,JNI 編程也同樣如此。程序員在使用 JNI 時(shí)應(yīng)當(dāng)認(rèn)識到 JNI 編程中如下的幾點(diǎn)弊端,揚(yáng)長避短,才可以寫出更加完善、高性能的代碼:
JAVA 編程中的內(nèi)存泄漏,從泄漏的內(nèi)存位置角度可以分為兩種:JVM 中 Java Heap 的內(nèi)存泄漏;JVM 內(nèi)存中 native memory 的內(nèi)存泄漏。 Java 對象存儲在 JVM 進(jìn)程空間中的 Java Heap 中,Java Heap 可以在 JVM 運(yùn)行過程中動態(tài)變化。如果 Java 對象越來越多,占據(jù) Java Heap 的空間也越來越大,JVM 會在運(yùn)行時(shí)擴(kuò)充 Java Heap 的容量。如果 Java Heap 容量擴(kuò)充到上限,并且在 GC 后仍然沒有足夠空間分配新的 Java 對象,便會拋出 out of memory 異常,導(dǎo)致 JVM 進(jìn)程崩潰。 Java Heap 中 out of memory 異常的出現(xiàn)有兩種原因——①程序過于龐大,致使過多 Java 對象的同時(shí)存在;②程序編寫的錯(cuò)誤導(dǎo)致 Java Heap 內(nèi)存泄漏。 多種原因可能導(dǎo)致 Java Heap 內(nèi)存泄漏。JNI 編程錯(cuò)誤也可能導(dǎo)致 Java Heap 的內(nèi)存泄漏。 JVM 中 native memory 的內(nèi)存泄漏 從操作系統(tǒng)角度看,JVM 在運(yùn)行時(shí)和其它進(jìn)程沒有本質(zhì)區(qū)別。在系統(tǒng)級別上,它們具有同樣的調(diào)度機(jī)制,同樣的內(nèi)存分配方式,同樣的內(nèi)存格局。 JVM 進(jìn)程空間中,Java Heap 以外的內(nèi)存空間稱為 JVM 的 native memory。進(jìn)程的很多資源都是存儲在 JVM 的 native memory 中,例如載入的代碼映像,線程的堆棧,線程的管理控制塊,JVM 的靜態(tài)數(shù)據(jù)、全局?jǐn)?shù)據(jù)等等。也包括 JNI 程序中 native code 分配到的資源。 在 JVM 運(yùn)行中,多數(shù)進(jìn)程資源從 native memory 中動態(tài)分配。當(dāng)越來越多的資源在 native memory 中分配,占據(jù)越來越多 native memory 空間并且達(dá)到 native memory 上限時(shí),JVM 會拋出異常,使 JVM 進(jìn)程異常退出。而此時(shí) Java Heap 往往還沒有達(dá)到上限。 多種原因可能導(dǎo)致 JVM 的 native memory 內(nèi)存泄漏。例如 JVM 在運(yùn)行中過多的線程被創(chuàng)建,并且在同時(shí)運(yùn)行。JVM 為線程分配的資源就可能耗盡 native memory 的容量。 JNI 編程錯(cuò)誤也可能導(dǎo)致 native memory 的內(nèi)存泄漏。對這個(gè)話題的討論是本文的重點(diǎn)。 JNI 編程實(shí)現(xiàn)了 native code 和 Java 程序的交互,因此 JNI 代碼編程既遵循 native code 編程語言的編程規(guī)則,同時(shí)也遵守 JNI 編程的文檔規(guī)范。在內(nèi)存管理方面,native code 編程語言本身的內(nèi)存管理機(jī)制依然要遵循,同時(shí)也要考慮 JNI 編程的內(nèi)存管理。 本章簡單概括 JNI 編程中顯而易見的內(nèi)存泄漏。從 native code 編程語言自身的內(nèi)存管理,和 JNI 規(guī)范附加的內(nèi)存管理兩方面進(jìn)行闡述。 JNI 編程首先是一門具體的編程語言,或者 C 語言,或者 C++,或者匯編,或者其它 native 的編程語言。每門編程語言環(huán)境都實(shí)現(xiàn)了自身的內(nèi)存管理機(jī)制。因此,JNI 程序開發(fā)者要遵循 native 語言本身的內(nèi)存管理機(jī)制,避免造成內(nèi)存泄漏。以 C 語言為例,當(dāng)用 malloc() 在進(jìn)程堆中動態(tài)分配內(nèi)存時(shí),JNI 程序在使用完后,應(yīng)當(dāng)調(diào)用 free() 將內(nèi)存釋放??傊?,所有在 native 語言編程中應(yīng)當(dāng)注意的內(nèi)存泄漏規(guī)則,在 JNI 編程中依然適應(yīng)。 Native 語言本身引入的內(nèi)存泄漏會造成 native memory 的內(nèi)存,嚴(yán)重情況下會造成 native memory 的 out of memory。 JNI 編程還要同時(shí)遵循 JNI 的規(guī)范標(biāo)準(zhǔn),JVM 附加了 JNI 編程特有的內(nèi)存管理機(jī)制。 JNI 中的 Local Reference 只在 native method 執(zhí)行時(shí)存在,當(dāng) native method 執(zhí)行完后自動失效。這種自動失效,使得對 Local Reference 的使用相對簡單,native method 執(zhí)行完后,它們所引用的 Java 對象的 reference count 會相應(yīng)減 1。不會造成 Java Heap 中 Java 對象的內(nèi)存泄漏。 而 Global Reference 對 Java 對象的引用一直有效,因此它們引用的 Java 對象會一直存在 Java Heap 中。程序員在使用 Global Reference 時(shí),需要仔細(xì)維護(hù)對 Global Reference 的使用。如果一定要使用 Global Reference,務(wù)必確保在不用的時(shí)候刪除。就像在 C 語言中,調(diào)用 malloc() 動態(tài)分配一塊內(nèi)存之后,調(diào)用 free() 釋放一樣。否則,Global Reference 引用的 Java 對象將永遠(yuǎn)停留在 Java Heap 中,造成 Java Heap 的內(nèi)存泄漏。 JNI 編程中潛在的內(nèi)存泄漏——對 LocalReference 的深入理解 Local Reference 在 native method 執(zhí)行完成后,會自動被釋放,似乎不會造成任何的內(nèi)存泄漏。但這是錯(cuò)誤的。對 Local Reference 的理解不夠,會造成潛在的內(nèi)存泄漏。 本章重點(diǎn)闡述 Local Reference 使用不當(dāng)可能引發(fā)的內(nèi)存泄漏。引入兩個(gè)錯(cuò)誤實(shí)例,也是 JNI 程序員容易忽視的錯(cuò)誤;在此基礎(chǔ)上介紹 Local Reference 表,對比 native method 中的局部變量和 JNI Local Reference 的不同,使讀者深入理解 JNI Local Reference 的實(shí)質(zhì);最后為 JNI 程序員提出應(yīng)該如何正確合理使用 JNI Local Reference,以避免內(nèi)存泄漏。 在某些情況下,我們可能需要在 native method 里面創(chuàng)建大量的 JNI Local Reference。這樣可能導(dǎo)致 native memory 的內(nèi)存泄漏,如果在 native method 返回之前 native memory 已經(jīng)被用光,就會導(dǎo)致 native memory 的 out of memory。 在代碼清單 1 里,我們循環(huán)執(zhí)行 count 次,JNI function NewStringUTF() 在每次循環(huán)中從 Java Heap 中創(chuàng)建一個(gè) String 對象,str 是 Java Heap 傳給 JNI native method 的 Local Reference,每次循環(huán)中新創(chuàng)建的 String 對象覆蓋上次循環(huán)中 str 的內(nèi)容。str 似乎一直在引用到一個(gè) String 對象。整個(gè)運(yùn)行過程中,我們看似只創(chuàng)建一個(gè) Local Reference。 執(zhí)行代碼清單 1 的程序,第一部分為 Java 代碼,nativeMethod(int i) 中,輸入?yún)?shù)設(shè)定循環(huán)的次數(shù)。第二部分為 JNI 代碼,用 C 語言實(shí)現(xiàn)了 nativeMethod(int i)。 清單 1. Local Reference 引發(fā)內(nèi)存泄漏
運(yùn)行結(jié)果證明,JVM 運(yùn)行異常終止,原因是創(chuàng)建了過多的 Local Reference,從而導(dǎo)致 out of memory。實(shí)際上,nativeMethod 在運(yùn)行中創(chuàng)建了越來越多的 JNI Local Reference,而不是看似的始終只有一個(gè)。過多的 Local Reference,導(dǎo)致了 JNI 內(nèi)部的 JNI Local Reference 表內(nèi)存溢出。 實(shí)例 2 是實(shí)例 1 的變種,Java 代碼未作修改,但是 nativeMethod(int i) 的 C 語言實(shí)現(xiàn)稍作修改。在 JNI 的 native method 中實(shí)現(xiàn)的 utility 函數(shù)中創(chuàng)建 Java 的 String 對象。utility 函數(shù)只建立一個(gè) String 對象,返回給調(diào)用函數(shù),但是 utility 函數(shù)對調(diào)用者的使用情況是未知的,每個(gè)函數(shù)都可能調(diào)用它,并且同一函數(shù)可能調(diào)用它多次。在實(shí)例 2 中,nativeMethod 在循環(huán)中調(diào)用 count 次,utility 函數(shù)在創(chuàng)建一個(gè) String 對象后即返回,并且會有一個(gè)退棧過程,似乎所創(chuàng)建的 Local Reference 會在退棧時(shí)被刪除掉,所以應(yīng)該不會有很多 Local Reference 被創(chuàng)建。實(shí)際運(yùn)行結(jié)果并非如此。 清單 2. Local Reference 引發(fā)內(nèi)存泄漏
運(yùn)行結(jié)果證明,實(shí)例 2 的結(jié)果與實(shí)例 1 的完全相同。過多的 Local Reference 被創(chuàng)建,仍然導(dǎo)致了 JNI 內(nèi)部的 JNI Local Reference 表內(nèi)存溢出。實(shí)際上,在 utility 函數(shù) CreateStringUTF(JNIEnv * env) 執(zhí)行完成后的退棧過程中,創(chuàng)建的 Local Reference 并沒有像 native code 中的局部變量那樣被刪除,而是繼續(xù)在 Local Reference 表中存在,并且有效。Local Reference 和局部變量有著本質(zhì)的區(qū)別。 Java JNI 的文檔規(guī)范只描述了 JNI Local Reference 是什么(存在的目的),以及應(yīng)該怎么使用 Local Reference(開放的接口規(guī)范)。但是對 Java 虛擬機(jī)中 JNI Local Reference 的實(shí)現(xiàn)并沒有約束,不同的 Java 虛擬機(jī)有不同的實(shí)現(xiàn)機(jī)制。這樣的好處是,不依賴于具體的 JVM 實(shí)現(xiàn),有好的可移植性;并且開發(fā)簡單,規(guī)定了“應(yīng)該怎么做、怎么用”。但是弊端是初級開發(fā)者往往看不到本質(zhì),“不知道為什么這樣做”。對 Local Reference 沒有深層的理解,就會在編程過程中無意識的犯錯(cuò)。 Local Reference 和 Local Reference 表 理解 Local Reference 表的存在是理解 JNI Local Reference 的關(guān)鍵。 JNI Local Reference 的生命期是在 native method 的執(zhí)行期(從 Java 程序切換到 native code 環(huán)境時(shí)開始創(chuàng)建,或者在 native method 執(zhí)行時(shí)調(diào)用 JNI function 創(chuàng)建),在 native method 執(zhí)行完畢切換回 Java 程序時(shí),所有 JNI Local Reference 被刪除,生命期結(jié)束(調(diào)用 JNI function 可以提前結(jié)束其生命期)。 實(shí)際上,每當(dāng)線程從 Java 環(huán)境切換到 native code 上下文時(shí)(J2N),JVM 會分配一塊內(nèi)存,創(chuàng)建一個(gè) Local Reference 表,這個(gè)表用來存放本次 native method 執(zhí)行中創(chuàng)建的所有的 Local Reference。每當(dāng)在 native code 中引用到一個(gè) Java 對象時(shí),JVM 就會在這個(gè)表中創(chuàng)建一個(gè) Local Reference。比如,實(shí)例 1 中我們調(diào)用 NewStringUTF() 在 Java Heap 中創(chuàng)建一個(gè) String 對象后,在 Local Reference 表中就會相應(yīng)新增一個(gè) Local Reference。 圖 1. Local Reference 表、Local Reference 和 Java 對象的關(guān)系 圖 1 中: ⑴運(yùn)行 native method 的線程的堆棧記錄著 Local Reference 表的內(nèi)存位置(指針 p)。 ⑵ Local Reference 表中存放 JNI Local Reference,實(shí)現(xiàn) Local Reference 到 Java 對象的映射。 ⑶ native method 代碼間接訪問 Java 對象(java obj1,java obj2)。通過指針 p 定位相應(yīng)的 Local Reference 的位置,然后通過相應(yīng)的 Local Reference 映射到 Java 對象。 ⑷當(dāng) native method 引用一個(gè) Java 對象時(shí),會在 Local Reference 表中創(chuàng)建一個(gè)新 Local Reference。在 Local Reference 結(jié)構(gòu)中寫入內(nèi)容,實(shí)現(xiàn) Local Reference 到 Java 對象的映射。 ⑸ native method 調(diào)用 DeleteLocalRef() 釋放某個(gè) JNI Local Reference 時(shí),首先通過指針 p 定位相應(yīng)的 Local Reference 在 Local Ref 表中的位置,然后從 Local Ref 表中刪除該 Local Reference,也就取消了對相應(yīng) Java 對象的引用(Ref count 減 1)。 ⑹當(dāng)越來越多的 Local Reference 被創(chuàng)建,這些 Local Reference 會在 Local Ref 表中占據(jù)越來越多內(nèi)存。當(dāng) Local Reference 太多以至于 Local Ref 表的空間被用光,JVM 會拋出異常,從而導(dǎo)致 JVM 的崩潰。 Local Ref 不是 native code 的局部變量 很多人會誤將 JNI 中的 Local Reference 理解為 Native Code 的局部變量。這是錯(cuò)誤的。 Native Code 的局部變量和 Local Reference 是完全不同的,區(qū)別可以總結(jié)為: ⑴局部變量存儲在線程堆棧中,而 Local Reference 存儲在 Local Ref 表中。 ⑵局部變量在函數(shù)退棧后被刪除,而 Local Reference 在調(diào)用 DeleteLocalRef() 后才會從 Local Ref 表中刪除,并且失效,或者在整個(gè) Native Method 執(zhí)行結(jié)束后被刪除。 ⑶可以在代碼中直接訪問局部變量,而 Local Reference 的內(nèi)容無法在代碼中直接訪問,必須通過 JNI function 間接訪問。JNI function 實(shí)現(xiàn)了對 Local Reference 的間接訪問,JNI function 的內(nèi)部實(shí)現(xiàn)依賴于具體 JVM。 代碼清單 1 中 str = (*env)->NewStringUTF(env, "0"); str 是 jstring 類型的局部變量。Local Ref 表中會新創(chuàng)建一個(gè) Local Reference,引用到 NewStringUTF(env, "0") 在 Java Heap 中新建的 String 對象。如圖 2 所示: 圖 2. str 間接引用 string 對象 圖 2 中,str 是局部變量,在 native method 堆棧中。Local Ref3 是新創(chuàng)建的 Local Reference,在 Local Ref 表中,引用新創(chuàng)建的 String 對象。JNI 通過 str 和指針 p 間接定位 Local Ref3,但 p 和 Local Ref3 對 JNI 程序員不可見。 Local Reference 導(dǎo)致內(nèi)存泄漏 在以上論述基礎(chǔ)上,我們通過分析錯(cuò)誤實(shí)例 1 和實(shí)例 2,來分析 Local Reference 可能導(dǎo)致的內(nèi)存泄漏,加深對 Local Reference 的深層理解。 分析錯(cuò)誤實(shí)例 1: 局部變量 str 在每次循環(huán)中都被重新賦值,間接指向最新創(chuàng)建的 Local Reference,前面創(chuàng)建的 Local Reference 一直保留在 Local Ref 表中。 在實(shí)例 1 執(zhí)行完第 i 次循環(huán)后,內(nèi)存布局如圖 3: 圖 3. 執(zhí)行 i 次循環(huán)后的內(nèi)存布局 繼續(xù)執(zhí)行完第 i+1 次循環(huán)后,內(nèi)存布局發(fā)生變化,如圖 4: 圖 4. 執(zhí)行 i+1 次循環(huán)后的內(nèi)存布局 圖 4 中,局部變量 str 被賦新值,間接指向了 Local Ref i+1。在 native method 運(yùn)行過程中,我們已經(jīng)無法釋放 Local Ref i 占用的內(nèi)存,以及 Local Ref i 所引用的第 i 個(gè) string 對象所占據(jù)的 Java Heap 內(nèi)存。所以,native memory 中 Local Ref i 被泄漏,Java Heap 中創(chuàng)建的第 i 個(gè) string 對象被泄漏了。 也就是說在循環(huán)中,前面創(chuàng)建的所有 i 個(gè) Local Reference 都泄漏了 native memory 的內(nèi)存,創(chuàng)建的所有 i 個(gè) string 對象都泄漏了 Java Heap 的內(nèi)存。 直到 native memory 執(zhí)行完畢,返回到 Java 程序時(shí)(N2J),這些泄漏的內(nèi)存才會被釋放,但是 Local Reference 表所分配到的內(nèi)存往往很小,在很多情況下 N2J 之前可能已經(jīng)引發(fā)嚴(yán)重內(nèi)存泄漏,導(dǎo)致 Local Reference 表的內(nèi)存耗盡,使 JVM 崩潰,例如錯(cuò)誤實(shí)例 1。 分析錯(cuò)誤實(shí)例 2: 實(shí)例 2 與實(shí)例 1 相似,雖然每次循環(huán)中調(diào)用工具函數(shù) CreateStringUTF(env) 來創(chuàng)建對象,但是在 CreateStringUTF(env) 返回退棧過程中,只是局部變量被刪除,而每次調(diào)用創(chuàng)建的 Local Reference 仍然存在 Local Ref 表中,并且有效引用到每個(gè)新創(chuàng)建的 string 對象。str 局部變量在每次循環(huán)中被賦新值。 這樣的內(nèi)存泄漏是潛在的,但是這樣的錯(cuò)誤在 JNI 程序員編程過程中卻經(jīng)常出現(xiàn)。通常情況,在觸發(fā) out of memory 之前,native method 已經(jīng)執(zhí)行完畢,切換回 Java 環(huán)境,所有 Local Reference 被刪除,問題也就沒有顯露出來。但是某些情況下就會引發(fā) out of memory,導(dǎo)致實(shí)例 1 和實(shí)例 2 中的 JVM 崩潰。 因此,在 JNI 編程時(shí),正確控制 JNI Local Reference 的生命期。如果需要創(chuàng)建過多的 Local Reference,那么在對被引用的 Java 對象操作結(jié)束后,需要調(diào)用 JNI function(如 DeleteLocalRef()),及時(shí)將 JNI Local Reference 從 Local Ref 表中刪除,以避免潛在的內(nèi)存泄漏。 本文闡述了 JNI 編程可能引發(fā)的內(nèi)存泄漏,JNI 編程既可能引發(fā) Java Heap 的內(nèi)存泄漏,也可能引發(fā) native memory 的內(nèi)存泄漏,嚴(yán)重的情況可能使 JVM 運(yùn)行異常終止。JNI 軟件開發(fā)人員在編程中,應(yīng)當(dāng)考慮以下幾點(diǎn),避免內(nèi)存泄漏:
|
|