JVM記憶體和Native記憶體
上面這張圖大家都應該很熟了,下面只講下和JNI有關的部分
程式計數器
記錄正在執行的虛擬機器位元組碼指令的地址(如果正在執行的是本地方法則為空)。
本地方法棧
本地方法棧與 Java 虛擬機器棧類似,它們之間的區別只不過是本地方法棧為本地方法服務。 本地方法一般是用其它語言(C、C++ 或組合語言等)編寫的,並且被編譯為基於本機硬體和作業系統的程式,對待這些方法需要特別處理。
堆(Java-Heap)
所有物件都在這裡分配記憶體,是垃圾收集的主要區域("GC 堆")。 堆不需要連續記憶體,並且可以動態增加其記憶體,增加失敗會丟擲 OutOfMemoryError 異常。
可以通過 -Xms 和 -Xmx 兩個虛擬機器引數來指定一個程式的堆記憶體大小,第一個引數設定初始值,第二個引數設定最大值。
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的主要作用如下:
- 管理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回收記憶體。
然而,JNI和上面兩者又有些區別。 JNI提供了與Java相對應的引用型別(如:jobject、jstring、jclass、jarray、jintArray等),以便Native程式碼可以通過JNI函式訪問到Java物件。引用所指向的Java物件通常就是存放在Java Heap,而Native程式碼持有的引用是存放在Native Memory中。
舉個例子,如下程式碼:
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 Reference
、Global Reference
、Weak 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環境一直被佔用。