【GiraKoo】Java Native Interface(JNI)的空間(引用)管理

GiraKoo發表於2023-05-18

Java Native Interface(JNI)的空間(引用)管理

Java是透過垃圾回收機制回收記憶體,C/C++是透過malloc,free,new,delete手動管理空間。那麼在JNI層,同時存在Java和C/C++的空間時,該如何進行空間的管理呢?本文參考Oracle的官方文件,對JNI層中空間的管理進行說明。明確哪些內容需要手動呼叫Delete,哪些不需要手動呼叫。

一、全域性引用(Global References)

全域性引用的生命週期(Lifetime),需要主動透過函式呼叫進行申請和釋放。native函式執行完畢後,該空間可繼續使用。

函式原型

// 建立全域性引用
jobject NewGlobalRef(JNIEnv *env, jobject obj);  

// 刪除全域性引用
void DeleteGlobalRef(JNIEnv *env, jobject globalRef);

二、區域性引用(Local References)

區域性引用的生命週期(Lifetime),與呼叫的native函式一致。當native函式return時,區域性引用將會被自動釋放。該區域性引用佔用的空間是JVM的資源。

由於native函式用於存放區域性引用的空間是固定的。如果過度的建立區域性引用,不加以控制,可能會出現空間不足,程式丟擲Out Of Memory(OOM)異常的問題。如果該區域性引用已經使用完畢,應儘量手動呼叫DeleteLocalRef,提前釋放空間,避免OOM。

函式原型

// 建立全域性引用
jobject NewLocalRef(JNIEnv *env, jobject obj);  

// 刪除全域性引用
void DeleteLocalRef(JNIEnv *env, jobject localRef);

// 主動設定可建立的區域性引用的數量
jint EnsureLocalCapacity(JNIEnv *env, jint capacity);

// 建立一個區域性引用的幀,並指定可建立的區域性引用的數量。
jint PushLocalFrame(JNIEnv *env, jint capacity);

// 釋放當前區域性引用的幀,釋放全部本地引用。
// 可以透過引數result,將被PopLocalFrame釋放的引用轉移到上一層的區域性引用幀中
jobject PopLocalFrame(JNIEnv *env, jobject result);

特殊說明

  1. 在JDK/JRE 1.1版本中,提供了DeleteLocalRef函式。
  2. 在JDK/JRE 1.2版本中,提供了EnsureLocalCapacityPushLocalFramePopLocalFrameNewLocalRef函式,支援更加複雜的區域性引用生命週期能力。

三、弱全域性引用(Weak Global Reference)

區別於全域性引用(Global References),該引用並不會增加該引用的引用計數。

即持有弱全域性引用,不會干預垃圾回收機制(GC)對於該記憶體的回收。適用於生命週期短的物件,持有生命週期長的引用。

全域性引用可以透過jboolean IsSameObject(JNIEnv *env, jobject ref1, jobject ref2); 函式,引數為被判定物件和NULL來判斷物件是否存在。如果返回JNI_TRUE判定該弱引用指向的物件是否已經被釋放。

使用弱全域性引用時,可能會出現執行一半,引用被GC回收的情況。所以需要在使用弱全域性引用之前,提供NewLocalRef或者NewGlobalRef,將弱引用轉換為強引用。確保GC不會對該空間進行回收。

四、常見問題

1、在native函式中,直接return一個jstring或者其他jobject,是否會有記憶體洩漏問題?

如果直接return的物件是區域性引用(Local Reference),則不會發生記憶體洩漏問題。
區域性引用在native函式執行完畢後,會根據區域性引用表(Local Reference Table)進行空間的釋放。
當然,手動執行DeleteLocalRef函式進行提前釋放,也不會產生問題。

2、什麼情況下可能會出現記憶體洩漏?

1)建立的全域性引用(Global Reference),未呼叫Delete函式進行回收。導致該物件引用計數無法歸零。
2)兩個物件互相持有對方的強引用。例如A持有B引用,B和C互相持有對方的引用。當A釋放B引用時,GC發現C依然持有B的引用,則不會釋放B。與此同時,GC也發現B持有C的引用,則不會釋放C。導致B和C與其他引用脫離,形成孤島。這塊記憶體將無法使用,也無法被釋放。

3、呼叫NewString和NewStringUTF時,是否需要釋放?

NewStringNewStringUTF函式建立的空間屬於區域性引用(Local Reference)。可以等待native函式執行完畢後自動回收,也可以在使用完畢(後續不會再訪問)後,直接進行Delete釋放。

但是需要注意,如果透過GetStringCharsGetStringCritical或者GetStringUTFChars獲取了jstring中的char* 空間,則需要使用對應的ReleaseStringCharsReleaseStringCritical或者ReleaseStringUTFChars釋放該char* 空間。

參考文章

相關文章