誰建立誰銷燬,誰分配誰釋放——JNI呼叫時的記憶體管理
在QQ音樂AndroidTV端的Cocos版本的開發過程中,我們希望儘量多的複用現有的業務邏輯,避免重複製造輪子。因此,我們使用了大量的JNI呼叫,來實現Java層和Native層(主要是C++)的程式碼通訊。一個重要的問題是JVM不會幫我們管理Native Memory所分配的記憶體空間的,本文就主要介紹如何在JNI呼叫時,對於Java層和Native層對映物件的記憶體管理策略。
1. 在Java層利用JNI呼叫Native層程式碼
如果有Java層嘗試呼叫Native層的程式碼,我們通常用Java物件來封裝C++的物件。舉個例子,在Java層的一個監聽播放狀態的類:MusicPlayListener,作用是將播放狀態傳送給位於Native層的Cocos,通知Cocos在介面上修改顯示圖示,例如“播放”,“暫停”等等。
第一種做法,是在Java類的建構函式中,呼叫Native層的建構函式,分配Native Heap的記憶體空間,之後,在Java類的finalize方法中呼叫Native層的解構函式,回收Native Heap的記憶體空間。
// in Java: public class MusicPlayListener { // 指向底層物件的指標,偽裝成Java的long private final long ptr; public MusicPlayListener() { ptr = ccCreate(); } // 在finalize裡釋放 public void finalize() { ccFree(ptr); } // 是否正在播放 public void setPlayState(boolean isPlaying){ ccSetPlayState(ptr,isPlaying); } private static native long ccCreate(); private static native void ccFree(long ptr); private native void ccSetPlayState(long ptr,boolean isPlaying); } // in C: jlong Java_MusicPlayListener_ccCreate(JNIEnv* env, jclass unused) { // 呼叫建構函式分配記憶體空間 CCMusicPlayListener* musicPlayListener = new CCMusicPlayListener(); return (jlong) musicPlayListener; } void Java_MusicPlayListener_ccFree( JNIEnv* env, jclass unused, jlong ptr) { // 釋放記憶體空間 delete ptr; } void Java_MusicPlayListener_ccSetPlayState( JNIEnv* env, jclass unused, jlong ptr, jboolean isPlaying) { //將播放狀態通知給UI執行緒 (reinterpret_cast<CCMusicPlayListener*>(ptr))->setPlayState(isPlaying); }
這種做法會讓Java物件和Native物件的生命週期保持一致,當Java物件在Java Heap中,被GC判定為回收時,同時會將Native Heap中的物件回收。
不透過finalize的話,也可以用其他類似的機制適用於上述場景。比如Java標準庫提供的DirectByteBuffer的實現,用基於PhantomReference的sun.misc.Cleaner來清理,本質上跟finalize方式一樣,只是比finalize稍微安全一點,他可以避免”懸空指標“的問題。
這種方式的一個重要缺點,就是不管是finalize還是其他類似的方法,都依賴於JVM的GC來處理的。換句話說,如果不觸發GC,那麼finalize方法就不會及時呼叫,這可能會導致Native Heap資源耗盡,而導致程式出錯。當Native層需要申請一個很大空間的記憶體時,有一定機率出現Native OutOfMemoryError的問題,然後找了半天也發現不了問題在哪裡...
第二種方法是對Api的一些簡單調整,以解決上述問題。不在JNI的包裝類的建構函式中初始化Native層物件,儘量寫成open/close的形式,在open的時候初始化Native資源,close的時候釋放,finalize作為最後的保險再檢查釋放一次。
雖然沒有本質上的變化,但open/close這種Api設計,一般來說,對90%的開發人員還是能夠提醒他們使用close的,至於剩下的10%...好像除了開除也沒啥好辦法了...
2. 在Native層利用JNI呼叫Java層程式碼
上一種情況,是以Java層為主導,Native層物件的生命週期受Java層物件的控制。下面要介紹的是另一種情況,即Native層物件為主導,由他控制Java層物件的生命週期。
2.1 Native層操作Java層物件
想要在native層操作Java Heap中的物件,需要位於Native層的引用(Reference)以指向Java Heap中的記憶體空間。JNI中為我們提供了三種引用:本地引用(Local Reference),全域性引用(Global Reference)和弱全域性引用(Weak Global Reference)。
Local Reference的生命週期持續到一個Native Method的結束,當Native Method返回時Java Heap中的物件不再被持有,等待GC回收。一定要注意不要在Native Method中申請過多的Local Reference,每個Local Reference都會佔用一定的JVM資源,過多的Local Reference會導致JVM記憶體溢位而導致Native Method的Crash。但是有些情況下我們必然會建立多個Local Reference,比如在一個對列表進行遍歷的迴圈體內,這時候開發人員有必要呼叫DeleteLocalRef手動清除不再使用的Local Reference。
//C++程式碼 class Coo{ public: void Foo(){ //獲得區域性引用物件ret jobject ret = env->CallObjectMethod(); for(int i =0;i<10;i++){ //獲得區域性引用物件cret jobject cret = env->CallObjectMethod(); //... //手動回收區域性引用物件cret env->DeleteLocalRef(cret); } } //native method 返回,區域性引用物件ret被自動回收 };
Global Reference的生命週期完全由程式設計師控制,你可以呼叫NewGlobalRef方法將一個Local Reference轉變為Global Reference,Global Reference的生命週期會一直持續到你顯式的呼叫DeleteGlobalRef,這有點像C++的動態記憶體分配,你需要記住new/delete永遠是成對出現的。
//C++程式碼 class Coo{ public: void Foo(){ //獲得區域性引用物件ret jobject ret = env->CallObjectMethod(); //獲的全域性引用物件gret jobject gret = env->NewGlobalRef(ret); }//native method 返回,區域性引用物件ret被自動回收 //gret不會回收,造成記憶體溢位 };
Weak Global Reference是一種特殊的Global Reference,它允許JVM在Java Heap執行GC時回收Native層所持有的Java物件,前提是這個物件除了Weak Reference以外,沒有被其他引用持有。我們在使用Weak Global Reference之前,可以使用IsSameObject來判斷位於Java Heap中的物件是否被釋放。
2.2 Native層釋放的同時釋放Java層物件
C++中的物件總會在其生命週期結束時,呼叫自身的解構函式,釋放動態分配的記憶體空間,Cocos利用資源釋放池(其本質是一種引用計數機制)來管理所有繼承自cocos2d::CCObject(3.2版本之後變為cocos::Ref)的物件。換言之,物件的生命週期交給Cocos管理,我們需要關心物件的析構過程。
一種簡單有效的做法,是在C++的建構函式中,例項化Java層的物件,在C++的解構函式中釋放Java層物件。舉個例子,主介面需要拉取Java層程式碼來解析後臺協議,獲取到主介面的幾個圖片的URL資訊。
先來看顯示效果:
再看程式碼:
//C++程式碼 class CCMainDeskListener { public: CCMainDeskListener(); ~CCMainDeskListener(); private: //Java層物件的全域性引用 jobject retGlobal; }; CCMainDeskListener::CCMainDeskListener() { //獲得本地引用 jobject ret = CallStaticObjectMethod(); //建立全域性引用 retGlobal = NewGlobalRef(ret); //清除本地引用 DeleteLocalRef(ret); } CCMainDeskListener::~CCMainDeskListener() { //清除全域性引用 DeleteGlobalRef(retGlobal); }
在C++的建構函式中,呼叫Java層的方法初始化了Java物件,這個引用分配的記憶體空間位於Java Heap。之後我們建立全域性引用,避免Local Reference在Native Method結束之後被回收,而全域性引用在解構函式中被刪除,這樣就保證了Java Heap中的物件被釋放,保持Native層和Java層的釋放做到同步。
上述方法中,Java層物件的生命週期是跟隨Native層物件的生命週期的,Native層物件的生命週期結束時會釋放對於Java層物件的持有,讓GC去回收資源。我們想進一步瞭解Native層物件的什麼時候被回收,接下來介紹一下Cocos的記憶體管理策略。
3.Cocos的記憶體管理
C++中,在堆上分配和釋放動態記憶體的方法是new和delete,程式設計師要小心的使用它們,確保每次呼叫了new之後,都有delete與之對應。為了避免因為遺漏delete而造成的記憶體洩露,C++標準庫(STL)提供了auto_ptr和shared_ptr,本質上都是用來確保當物件的生命週期結束時,堆上分配的記憶體被釋放。
Cocos採用的是引用計數的記憶體管理方式,這已經是一種十分古老的管理方式了,不過這種方式簡單易實現,當物件的引用次數減為0時,就呼叫delete方法將物件清除掉。具體實現上來說,Cocos會為每個程式建立一個全域性的CCAutoreleasePool類,開發人員不能自己建立釋放池,僅僅需要關注release和retain方法,不過前提是你的物件必須要繼承自cocos2d::CCObject類(3.0版本之後變為cocos2d::Ref類),這個類是Cocos所有物件繼承的基類,有點類似於Java的Object類。
當你呼叫object->autorelease()方法時,物件就被放到了自動釋放池中,自動釋放池會幫助你保持這個obejct的生命週期,直到當前訊息迴圈的結束。在這個訊息迴圈的最後,假如這個object沒有被其他類或容器retain過,那麼它將自動釋放掉。例如,layer->addChild(sprite),這個sprite增加到這個layer的子節點列表中,他的宣告週期就會持續到這個layer釋放的時候,而不會在當前訊息迴圈的最後被釋放掉。
跟記憶體管理有關的方法,一共有三個:release(),retain()和autorelease()。release和retain的作用分別是將當前引用次數減一和加一,autorelease的作用則是將當前物件的管理交給PoolManager。當物件的引用次數減為0時,PoolManager就會呼叫delete,回收記憶體空間。
release和retain的作用分別是將當前引用次數減一和加一,autorelease的作用則是將當前物件的管理交給PoolManager。當物件的引用次數減為0時,PoolManager就會呼叫delete,回收記憶體空間。
一般情況下,我們需要記住的就是繼承自Ref的物件,使用create方法建立例項後,是不需要我們手動delete的,因為create方法會自己呼叫autorelease方法。
4.總結
JNI呼叫時,即可能造成Native Heap的溢位,也可能造成Java Heap的溢位,作為JNI軟體開發人員,應該注意以下幾點:
Native層(一般是C++)本身的記憶體管理。
不使用的Global Reference和Local Reference都要及時釋放。
Java層呼叫JNI時儘量使用open/close的格式替代建構函式/finalize的方式。
來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/31557897/viewspace-2671498/,如需轉載,請註明出處,否則將追究法律責任。
相關文章
- 直接記憶體和堆記憶體誰快記憶體
- 豐田經驗:誰做就誰改、誰改就誰制定標準!
- Cookie 由誰建立Cookie
- 誰掌握了工具誰便是強者 誰掌握了資訊誰便是勝者
- React和Vue誰會淘汰誰?ReactVue
- 誰動了我的記憶體之 PHP 記憶體溢位PHP記憶體溢位
- 誰動了我的記憶體之PHP記憶體溢位PHP記憶體溢位
- 誰該向誰學習(轉貼)
- 誰手握賬本?趣講 ZK 的記憶體模型記憶體模型
- 誰多誰少;及死亡小島分析
- 誰再黑程式設計師我就打誰程式設計師
- Google回來了!誰在期待?誰在恐慌?Go
- 輕量迅捷時代,Vite 與Webpack 誰贏誰輸ViteWeb
- 誰來解釋解釋場景。
- 誰負責業務知識的管理?
- 選擇旅遊國家;及誰多誰少分析
- 人與機器新思考:誰是主人誰又是寵物?
- 果粉放棄iPhone後 都投奔了誰?iPhone
- 我為誰工作
- 誰來取代Google?誰會是下一個歸來的王者?Go
- 「看圖」誰想幹掉誰?程式語言相愛相殺何時休
- 追蹤網賺遊戲:是誰割了你,而你又割了誰?遊戲
- PostgreSQL鎖等待監控珍藏級SQL-誰堵塞了誰SQL
- L1-096 誰管誰叫爹 分數 20
- 有誰開發過專案管理的軟體嗎?專案管理
- 查詢git某個分支是誰建立的Git
- 誰再把IDEA的Project比作Eclipse的Workspace,我就跟誰急IdeaProjectEclipse
- 起底中國遊戲2020上半年:誰在賺錢?誰在虧錢?誰又在討飯?遊戲
- 私鑰和公鑰到底是誰來加密、誰來解密?加密解密
- 誰說我們程式設計師悶騷,我跟誰急程式設計師
- 誰是中國的遊戲之都?遊戲
- 誰動了我的 Redis ?Redis
- 誰動了我的MySQL?MySql
- 誰說 JavaScript 簡單的?JavaScript
- 你打算賺誰的錢?
- 傳聞:誰控制了前端入口,誰就是IT行業的主宰!看完驚呆前端行業
- 誰掌握了感測器,誰就掌握了物聯網的未來
- JS中 this 到底指向誰?JS