百家乐必知技巧
開發

誰創建誰銷毀,誰分配誰釋放——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。但是有些情況下我們必然會創建多個LocalReference,比如在一個對列表進行遍歷的循環體內,這時候開發人員有必要調用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的方式。
我還沒有學會寫個人說明!

八年之癢!除了NLP和CV,人工智能就不能干點別的啥了?

上一篇

Nacos 服務注冊與發現原理分析

下一篇

你也可能喜歡

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

長按儲存圖像,分享給朋友

ITPUB 每周精要將以郵件的形式發放至您的郵箱


微信掃一掃

微信掃一掃
百家乐必知技巧 彩友体彩p3预测汇总 安徽十一选五 排列五预测 幸运赛车 卖给女人什么东西赚钱 澳洲幸运5是哪个国家的品牌 缩水软件在线 2012奥运会足球直播 安徽11选5助手下载 福建快三基本走势图一定牛