天天看點

在 JNI 程式設計中避免記憶體洩漏

簡介: 本文詳細論述如何在 jni 程式設計中避免記憶體洩漏。論述了 jni 程式設計中可能引發的明顯的記憶體洩漏。本文的重點是闡述 jni 程式設計中潛在的記憶體洩漏,希望讀者通過本文對 local reference 有更深刻的了解,了解 local reference 表的存在,區分 local reference 和局部變量,進而認識到 local reference 可能引發的 native memory 記憶體洩漏

<a>jni 程式設計簡介</a>

jni,java native interface,是 native code 的程式設計接口。jni 使 java 代碼程式可以與 native code 互動——在 java 程式中調用 native code;在 native code 中嵌入 java 虛拟機調用 java 的代碼。

jni 程式設計在軟體開發中運用廣泛,其優勢可以歸結為以下幾點:

利用 native code 的平台相關性,在平台相關的程式設計中彰顯優勢。

對 native code 的代碼重用。

native code 底層操作,更加高效。

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

從 java 環境到 native code 的上下文切換耗時、低效。

jni 程式設計,如果操作不當,可能引起 java 虛拟機的崩潰。

jni 程式設計,如果操作不當,可能引起記憶體洩漏。

<a href="http://www.ibm.com/developerworks/cn/java/j-lo-jnileak/index.html#ibm-pcon">回頁首</a>

<a>java 中的記憶體洩漏</a>

java 程式設計中的記憶體洩漏,從洩漏的記憶體位置角度可以分為兩種:jvm 中 java heap 的記憶體洩漏;jvm 記憶體中 native memory 的記憶體洩漏。

<a>java heap 的記憶體洩漏</a>

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 的記憶體洩漏。

<a>jvm 中 native memory 的記憶體洩漏</a>

從作業系統角度看,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 的記憶體洩漏。對這個話題的讨論是本文的重點。

<a>jni 程式設計中明顯的記憶體洩漏</a>

jni 程式設計實作了 native code 和 java 程式的互動,是以 jni 代碼程式設計既遵循 native code 程式設計語言的程式設計規則,同時也遵守 jni 程式設計的文檔規範。在記憶體管理方面,native code 程式設計語言本身的記憶體管理機制依然要遵循,同時也要考慮 jni 程式設計的記憶體管理。

本章簡單概括 jni 程式設計中顯而易見的記憶體洩漏。從 native code 程式設計語言自身的記憶體管理,和 jni 規範附加的記憶體管理兩方面進行闡述。

<a>native code 本身的記憶體洩漏</a>

jni 程式設計首先是一門具體的程式設計語言,或者 c 語言,或者 c++,或者彙編,或者其它 native 的程式設計語言。每門程式設計語言環境都實作了自身的記憶體管理機制。是以,jni 程式開發者要遵循 native 語言本身的記憶體管理機制,避免造成記憶體洩漏。以 c 語言為例,當用 malloc() 在程序堆中動态配置設定記憶體時,jni 程式在使用完後,應當調用 free() 将記憶體釋放。總之,所有在 native 語言程式設計中應當注意的記憶體洩漏規則,在 jni 程式設計中依然适應。

native 語言本身引入的記憶體洩漏會造成 native memory 的記憶體,嚴重情況下會造成 native memory 的 out of memory。

<a>global reference 引入的記憶體洩漏</a>

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 的記憶體洩漏。

<a>jni 程式設計中潛在的記憶體洩漏——對 localreference 的深入了解</a>

local reference 在 native method 執行完成後,會自動被釋放,似乎不會造成任何的記憶體洩漏。但這是錯誤的。對 local reference 的了解不夠,會造成潛在的記憶體洩漏。

本章重點闡述 local reference 使用不當可能引發的記憶體洩漏。引入兩個錯誤執行個體,也是 jni 程式員容易忽視的錯誤;在此基礎上介紹 local reference 表,對比 native method 中的局部變量和 jni local reference 的不同,使讀者深入了解 jni local reference 的實質;最後為 jni 程式員提出應該如何正确合理使用 jni local reference,以避免記憶體洩漏。

<a>錯誤執行個體 1</a>

在某些情況下,我們可能需要在 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)。

<a>清單 1. local reference 引發記憶體洩漏</a>

運作結果證明,jvm 運作異常終止,原因是建立了過多的 local reference,進而導緻 out of memory。實際上,nativemethod 在運作中建立了越來越多的 jni local reference,而不是看似的始終隻有一個。過多的 local reference,導緻了 jni 内部的 jni local reference 表記憶體溢出。

<a>錯誤執行個體 2</a>

執行個體 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 被建立。實際運作結果并非如此。

<a>清單 2. local reference 引發記憶體洩漏</a>

運作結果證明,執行個體 2 的結果與執行個體 1 的完全相同。過多的 local reference 被建立,仍然導緻了 jni 内部的 jni local reference 表記憶體溢出。實際上,在 utility 函數 createstringutf(jnienv * env)

執行完成後的退棧過程中,建立的 local reference 并沒有像 native code 中的局部變量那樣被删除,而是繼續在 local reference 表中存在,并且有效。local reference 和局部變量有着本質的差別。

<a>local reference 深層解析</a>

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。

<a>圖 1. local reference 表、local reference 和 java 對象的關系</a>

在 JNI 程式設計中避免記憶體洩漏

圖 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)-&gt;newstringutf(env, "0");

str 是 jstring 類型的局部變量。local ref 表中會新建立一個 local reference,引用到 newstringutf(env, "0") 在 java heap 中建立的 string 對象。如圖 2 所示:

<a>圖 2. str 間接引用 string 對象</a>

在 JNI 程式設計中避免記憶體洩漏

圖 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:

<a>圖 3. 執行 i 次循環後的記憶體布局</a>

在 JNI 程式設計中避免記憶體洩漏

繼續執行完第 i+1 次循環後,記憶體布局發生變化,如圖 4:

<a>圖 4. 執行 i+1 次循環後的記憶體布局</a>

在 JNI 程式設計中避免記憶體洩漏

圖 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 崩潰。

<a>控制 local reference 生命期</a>

是以,在 jni 程式設計時,正确控制 jni local reference 的生命期。如果需要建立過多的 local reference,那麼在對被引用的 java 對象操作結束後,需要調用 jni function(如 deletelocalref()),及時将 jni local reference 從 local ref 表中删除,以避免潛在的記憶體洩漏。

<a>總結</a>

本文闡述了 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 規範書中的使用規則。