JNI(Java Native Interface)讓 Java 與 C/C++ 共舞,但也帶來雙重內存管理負擔:Java 堆與Native 堆。稍有不慎,輕則 OOM,重則 JVM Crash。本文系統梳理三種引用類型生命週期、釋放策略,並針對性給出防洩漏守則。
| 引用類型 | 創建函數 | 釋放函數 | 作用域 | 阻止 GC | 跨線程 |
|---|---|---|---|---|---|
| 局部引用 | 大多數 JNI 函數(如 NewObject) | 自動釋放 / DeleteLocalRef | 當前 native 方法調用 | 是 | 否(線程私有) |
| 全局引用 | NewGlobalRef | DeleteGlobalRef | 進程全局,直到顯式釋放 | 是 | 是 |
| 弱全局引用 | NewWeakGlobalRef | DeleteWeakGlobalRef | 進程全局,直到顯式釋放 | 否(允許 GC 回收對象) | 是 |
局部引用僅在創建它的 native 方法幀中有效。方法返回時 JVM 自動清理,但循環大量創建或長時間運行的 native 方法會導致局部引用表溢出。
// 危險模式:循環內累積局部引用
for (int i = 0; i < hugeCount; i++) {
jstring s = (*env)->NewStringUTF(env, "data");
// 缺少 DeleteLocalRef → 局部引用表溢出
}
// 安全模式:及時釋放
for (int i = 0; i < hugeCount; i++) {
jstring s = (*env)->NewStringUTF(env, "data");
process(s);
(*env)->DeleteLocalRef(env, s); // 手動釋放
}
進階技巧:使用 PushLocalFrame/PopLocalFrame 建立嵌套作用域,批量自動釋放:
(*env)->PushLocalFrame(env, 64); // 預留 64 個槽位
// ... 大量局部引用操作 ...
(*env)->PopLocalFrame(env, NULL); // 一鍵清空該幀所有局部引用
跨調用、跨線程共享對象的利器,但永久阻止 GC。必須配對 DeleteGlobalRef,否則 Java Heap 洩漏必現。
// 正確緩存 Class 對象
static jclass cachedCls = NULL;
jclass getStringClass(JNIEnv *env) {
if (cachedCls == NULL) {
jclass local = (*env)->FindClass(env, "java/lang/String");
cachedCls = (*env)->NewGlobalRef(env, local); // 升級為全局引用
(*env)->DeleteLocalRef(env, local); // 釋放臨時局部引用
}
return cachedCls;
}
// 必須在庫卸載或進程結束時調用 DeleteGlobalRef(env, cachedCls)
適用於 Class 緩存、ClassLoader 可卸載場景。使用前必須用 IsSameObject(env, weakRef, NULL) 檢查對象是否存活(返回 JNI_TRUE 表示已被回收)。
malloc/new 未 free/deleteGetStringUTFChars 未 ReleaseStringUTFCharsNewXxxRef/DeleteXxxRef。DeleteLocalRef 或用 Push/PopLocalFrame。JNI_OnUnload 或關閉鉤子中清理。IsSameObject。goto cleanup 或 RAII 確保所有分支釋放資源。NewGlobalRef 是否有對應 DeleteGlobalRef?GetStringCritical/GetPrimitiveArrayCritical 是否極短時間內釋放?掌握引用類型語義、建立嚴格釋放紀律,是寫出穩健 JNI 代碼的關鍵。工欲善其事,必先利其器——AddressSanitizer、Valgrind、JVM -verbose:jni 都是排查洩漏的利器,善用之。