JNI內存管理及優化

JVM內存和Native內存

上面這張圖你們都應該很熟了,下面只講下和JNI有關的部分html

程序計數器

記錄正在執行的虛擬機字節碼指令的地址(若是正在執行的是本地方法則爲空)。java

本地方法棧

本地方法棧與 Java 虛擬機棧相似,它們之間的區別只不過是本地方法棧爲本地方法服務。 本地方法通常是用其它語言(C、C++ 或彙編語言等)編寫的,而且被編譯爲基於本機硬件和操做系統的程序,對待這些方法須要特別處理。 android

堆(Java-Heap)

全部對象都在這裏分配內存,是垃圾收集的主要區域("GC 堆")。 堆不須要連續內存,而且能夠動態增長其內存,增長失敗會拋出 OutOfMemoryError 異常。git

能夠經過 -Xms 和 -Xmx 兩個虛擬機參數來指定一個程序的堆內存大小,第一個參數設置初始值,第二個參數設置最大值。github

java -Xmx1024m -Xms1024m
//-Xmx1024m:設置JVM最大可用內存爲1024M。
//-Xms1024m:設置JVM初始內存爲1024m。此值可與-Xmx相同,以免每次垃圾回收完成後JVM從新分配內存。
複製代碼

在Android系統對於每一個應用都有內存使用的限制,機器的內存限制,在/system/build.prop文件中配置的。能夠在manifest文件application節點加入 android:largeHeap="true"來讓Dalvik/ART虛擬機分配更大的堆內存空間編程

直接內存(native堆)

也稱爲C-Heap,供Java Runtime進程使用的,沒有相應的參數來控制其大小,其大小依賴於操做系統進程的最大值。  Java應用程序都是在Java Runtime Environment(JRE)中運行,而Runtime自己就是由Native語言(如:C/C++)編寫程序。Native Memory就是操做系統分配給Runtime進程的可用內存,它與Heap Memory不一樣,Java Heap 是Java應用程序的內存。。Native Memory的主要做用以下:bash

  • 管理java heap的狀態數據(用於GC);
  • JNI調用,也就是Native Stack;
  • JIT(即便編譯器)編譯時使用Native Memory,而且JIT的輸入(Java字節碼)和輸出(可執行代碼)也都是保存在Native Memory;
  • NIO direct buffer;
  • Threads;
  • 類加載器和類信息都是保存在Native Memory中的。

JNI內存

在Java代碼中,Java對象被存放在JVM的Java Heap,由垃圾回收器(Garbage Collector,即GC)自動回收就能夠。多線程

 在Native代碼中,內存是從Native Memory中分配的,須要根據Native編程規範去操做內存。如:C/C++使用malloc()/new分配內存,須要手動使用free()/delete回收內存。app

 然而,JNI和上面二者又有些區別。 JNI提供了與Java相對應的引用類型(如:jobject、jstring、jclass、jarray、jintArray等),以便Native代碼能夠經過JNI函數訪問到Java對象。引用所指向的Java對象一般就是存放在Java Heap,而Native代碼持有的引用是存放在Native Memory中。jvm

舉個例子,以下代碼:

jstring jstr = env->NewStringUTF("Hello World!");
複製代碼
  • jstring類型是JNI提供的,對應於Java的String類型
  • JNI函數NewStringUTF()用於構造一個String對象,該對象存放在Java Heap中,同時返回了一個jstring類型的引用。
  • String對象的引用保存在jstr中,jstr是Native的一個局部變量,存放在Native Memory中。

開發人員都應該遇到過OOM(Out of Memory)異常,在JNI開發中,該異常可能發生在Java Heap中,也可能發生在Native Memory中。

java.lang.OutOfMemoryError: Java heap space
java.lang.OutOfMemoryError: native memory exhausted
複製代碼

Java Heap 中出現 Out of Memory異常的緣由有兩種:

1)程序過於龐大,導致過多 Java 對象的同時存在;
2)程序編寫的錯誤致使 Java Heap 內存泄漏。
複製代碼

Native Memory中出現 Out of Memory異常的緣由:

1)程序申請過多資源,系統未能知足,好比說大量線程資源;
2)程序編寫的錯誤致使Native Memory內存泄漏。
複製代碼

JNI引用

JNI引用有三種:Local ReferenceGlobal ReferenceWeak Global Reference。下面分別來介紹一下這三種引用內存分配和管理。

Local Reference

只在Native Method執行時存在,只在建立它的線程有效,不能跨線程使用。它的生命期是在Native Method的執行期開始建立(從Java代碼切換到Native代碼環境時,或者在Native Method執行時調用JNI函數時),在Native Method執行完畢切換回Java代碼時,全部Local Reference被刪除(GC會回收其內存),生命期結束(調用DeleteLocalRef()能夠提早回收內存,結束其生命期)。

 實際上,每當線程從Java環境切換到Native代碼環境時,JVM 會分配一塊內存用於建立一個Local Reference Table,這個Table用來存放本次Native Method 執行中建立的全部Local Reference。每當在 Native代碼中引用到一個Java對象時,JVM 就會在這個Table中建立一個Local Reference。好比,咱們調用 NewStringUTF() 在 Java Heap 中建立一個 String 對象後,在 Local Reference Table 中就會相應新增一個 Local Reference

Local Reference 表、Local Reference 和 Java 對象的關係

接下來舉個簡單例子說明一下:

jstring jstr = env->NewStringUTF("Hello World!");
複製代碼
  • jstr存放在Native Method Stack中,是一個局部變量
  • 對於開發者來講,Local Reference Table是不可見的
  • Local Reference Table的內存不大,所能存放的Local Reference數量也是有限的(在Android中默認最大容量是512個),使用不當就會引發溢出異常
  • Local Reference並非Native裏面的局部變量,局部變量存放在堆棧中,其引用存放在Local Reference Table中。

在Native Method結束時,JVM會自動釋放Local Reference,但Local Reference Table是有大小限制的,在開發中應該及時使用DeleteLocalRef()刪除沒必要要的Local Reference,否則可能會出現溢出錯誤:

JNI ERROR (app bug): local reference table overflow (max=512)
複製代碼

在C/C++中實例化的JNI對象,若是不返回java,必須用release掉或delete,不然內存泄露。包括NewStringUTF,NewObject。對於通常的基本數據類型(如:jint,jdouble等),是不必調用該函數刪除掉的。若是返回java沒必要delete,java會本身回收。

Global Reference

Local Reference是在Native Method執行的時候出現的,而Global Reference是經過JNI函數NewGlobalRef()DeleteGlobalRef()來建立和刪除的。 Global Reference具備全局性,能夠在多個Native Method調用過程和多線程中使用,在主動調用DeleteGlobalRef以前,它是一直有效的(GC不會回收其內存)。

/**
 * 建立obj參數所引用對象的新全局引用。obj參數既能夠是全局引用,也能夠是局部引用。全局引用經過調用DeleteGlobalRef()來顯式撤消。
 * @param obj 全局或局部引用。
 * @return 返回全局引用。若是系統內存不足則返回 NULL。
*/
jobject NewGlobalRef(jobject obj);

/**
 * 刪除globalRef所指向的全局引用
 * @param globalRef 全局引用
*/
void DeleteGlobalRef(jobject globalRef);
複製代碼

使用 Global reference時,當 native code 再也不須要訪問Global reference 時,應當調用 JNI 函數 DeleteGlobalRef() 刪除 Global reference和它引用的 Java 對象。不然Global Reference引用的 Java 對象將永遠停留在 Java Heap 中,從而致使 Java Heap 的內存泄漏。

Weak Global Reference

NewWeakGlobalRef()DeleteWeakGlobalRef()進行建立和刪除,它與Global Reference的區別在於該類型的引用隨時均可能被GC回收。

於Weak Global Reference而言,能夠經過isSameObject()將其與NULL比較,看看是否已經被回收了。若是返回JNI_TRUE,則表示已經被回收了,須要從新初始化弱全局引用。Weak Global Reference的回收時機是不肯定的,有可能在前一行代碼判斷它是可用的,後一行代碼就被GC回收掉了。爲了不這事事情發生,JNI官方給出了正確的作法,經過NewLocalRef()獲取Weak Global Reference,避免被GC回收。

注意點

Local Reference 不是 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。

注意釋放全部對jobject的引用:

extern "C"
JNIEXPORT jstring JNICALL Java_com_test_application_MainActivity_init(JNIEnv *env, jobject instance, jstring data, jbyteArray array) {

    int len = env->GetArrayLength(array);
    const char *utfChars = env->GetStringUTFChars(data, 0);
    jbyte *arrayElements = env->GetByteArrayElements(array, NULL);

    jstring pJstring = env->NewStringUTF(utfChars); 

    jbyteArray jpicArray = env->NewByteArray(len);
    env->SetByteArrayRegion(jpicArray, 0, len, arrayElements);
    
    // TODO
    
    env->DeleteLocalRef(pJstring);
    env->DeleteLocalRef(jpicArray);

    env->ReleaseStringUTFChars(data, utfChars);
    env->ReleaseByteArrayElements(array, arrayElements, 0);

    std::string hello = "Hello from C++";
    jstring result = env->NewStringUTF(hello.c_str());
    return result;
}
複製代碼

其它的還有:

jclass ref= (env)->FindClass("java/lang/String");
 
env->DeleteLocalRef(ref);
複製代碼

由於根據jni.h裏的定義:

typedef jobject         jclass;
複製代碼

jclass也是jobject。而jmethodID/jfielID和jobject沒有繼承關係,它們不是object,只是個整數,不存在被釋放與否的問題。

局部引用和全局引用的轉換

注意Local Reference的生命週期,若是在Native中須要長時間持有一個Java對象,就不能使用將jobject存儲在Native,不然在下次使用的時候,即便同一個線程調用,也將會沒法使用。下面是錯誤的作法:

jstring global;

extern "C" JNIEXPORT jstring JNICALL
Java_org_hik_libyuv_MainActivity_stringFromJNI(
        JNIEnv *env,
        jobject /* this */) {
    std::string hello = "Hello from C++";
    jstring local = env->NewStringUTF(hello.c_str());
    global = local;
    return local;
}


複製代碼

正確的作法是使用Global Reference,以下:

jstring global;

extern "C" JNIEXPORT jstring JNICALL
Java_org_hik_libyuv_MainActivity_stringFromJNI(
        JNIEnv *env,
        jobject /* this */) {
    std::string hello = "Hello from C++";
    jstring local = env->NewStringUTF(hello.c_str());
    global = static_cast<jstring>(env->NewGlobalRef(global));
    return local;
}
複製代碼

多線程

JNIEnv和jobject對象都不能跨線程使用。 對於jobject,解決辦法是

a、m_obj = env->NewGlobalRef(obj);//建立一個全局變量  

b、jobject obj = env->AllocObject(m_cls);//在每一個線程中都生成一個對象
複製代碼

對於JNIEnv,解決辦法是在每一個線程中都從新生成一個env

JavaVM *gJavaVM;//聲明全局變量
(*env)->GetJavaVM(env, &gJavaVM);//在JNI方法的中賦值

JNIEnv *env;//在其它線程中獲取當前線程的env  
m_jvm->AttachCurrentThread((void **)&env, NULL);  
複製代碼

當在一個線程裏面調用AttachCurrentThread後,若是不須要用的時候必定要DetachCurrentThread,不然線程沒法正常退出,致使JNI環境一直被佔用。

參考文章

C++調用JAVA方法詳解

JNI內存管理

Java 虛擬機

相關文章
相關標籤/搜索