誰創建誰銷燬,誰分配誰釋放——JNI調用時的內存管理

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軟件開發人員,應該注意以下幾點:

  1. Native層(一般是C++)本身的內存管理。
  2. 不使用的Global Reference和Local Reference都要及時釋放。
  3. Java層調用JNI時儘量使用open/close的格式替代構造函數/finalize的方式。

轉自:https://www.tinymind.net.cn/articles/acaf06f998d176

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章