JNI (Java Native Interface) 調用是 Java 與 C/C++ 交互的橋樑,但也是 JVM 崩溃的高發區。本文以實際 EXCEPTION_ACCESS_VIOLATION (0xc0000005) 崩潰日誌為例,深度解析崩潰堆疊、定位.native 代碼缺陷、提供系統化排查清單與避坑指南。
public class JNITransObject {
public native TestJNI[] ObjectMethod(String text);
static { System.loadLibrary("JNITransObject"); }
public static void main(String[] args) {
JNITransObject jni = new JNITransObject();
TestJNI[] arr = jni.ObjectMethod("jni trans object");
for (TestJNI obj : arr) {
System.out.println(obj.getTopic()); // 崩潰點
}
}
}
# A fatal error has been detected by the Java Runtime Environment:
# EXCEPTION_ACCESS_VIOLATION (0xc0000005) at pc=0x6d91c1db, pid=3544, tid=5408
# Problematic frame:
# V [jvm.dll+0x11c1db] <- JVM 內部代碼
# Java frames:
j java.lang.System.arraycopy(Ljava/lang/Object;ILjava/lang/Object;II)V+0
j java.lang.String.getChars(II[CI)V+62
j java.io.BufferedWriter.write(Ljava/lang/String;II)V+63
j java.io.Writer.write(Ljava/lang/String;)V+7
j java.io.PrintStream.write(Ljava/lang/String;)V+13
j java.io.PrintStream.print(Ljava/lang/String;)V+9
j java.io.PrintStream.println(Ljava/lang/String;)V+6
j jni.JNITransObject.main([Ljava/lang/String;)V+29
v ~StubRoutines::call_stub
System.out.println(obj.getTopic()) → 字符串獲取字符 → System.arraycopy → JVM 內部訪問違規。0xc0000005 = 讀取非法內存地址 (0x00000009),典型的空指針解引用或對象已被回收/損壞。ObjectMethod 返回的 TestJNI[] 數組或其元素內部指針無效。| 錯誤類型 | 典型代碼缺陷 | 後果 |
|---|---|---|
| 局部引用越界/釋放後使用 | 未調用 NewGlobalRef 就長期持有 jobject;DeleteLocalRef 後繼續用 | 指針懸空,訪問已回收內存 |
| 對象創建失敗未檢查 | FindClass/NewObject 返回 NULL 直接使用 | 空指針解引用 |
| 字段/方法 ID 緩存失效 | 類卸載後重載,舊 jfieldID/jmethodID 失效 | 訪問錯誤偏移量 |
| 數組操作邊界錯誤 | SetObjectArrayElement 索引越界;GetArrayLength 未檢查 | 內存越界寫入/讀取 |
| 字符串編碼錯誤 | NewStringUTF 處理非修正 UTF-8;GetStringUTFChars 未 Release | 內存洩漏、字符串截斷、崩潰 |
| 異常拋出未處理 | JNI 代碼拋出異常後未 ExceptionClear 繼續執行 | 異常傳播到 JVM 內部導致崩潰 |
| 線程附著/分離錯誤 | Native 線程未 AttachCurrentThread 直接調用 JNI | JNIEnv 無效,隨機崩潰 |
推斷:ObjectMethod 返回的 TestJNI[] 中的元素,其 topic 字段指向的 jstring 是局部引用,在 native 方法返回前已被 JVM 自動釋放,Java 端拿到的是野指針。
JNIEXPORT jobjectArray JNICALL Java_JNITransObject_ObjectMethod
(JNIEnv *env, jobject thisObj, jstring text) {
jclass cls = env->FindClass("TestJNI");
jmethodID ctor = env->GetMethodID(cls, "", "()V");
jfieldID fid = env->GetFieldID(cls, "topic", "Ljava/lang/String;");
jobjectArray arr = env->NewObjectArray(2, cls, NULL);
for (int i = 0; i < 2; i++) {
jobject obj = env->NewObject(cls, ctor);
// 錯誤:NewStringUTF 返回局部引用,循環結束/方法返回後自動失效!
jstring topic = env->NewStringUTF(("Topic " + std::to_string(i)).c_str());
env->SetObjectField(obj, fid, topic);
env->SetObjectArrayElement(arr, i, obj);
// 缺少 NewGlobalRef(topic) 或在 Java 端復制字符串
}
return arr;
}
// 方案 1:在 Java 端復制字符串 (推薦,無需 GlobalRef)
jstring topic = env->NewStringUTF(...);
jstring topicCopy = (jstring) env->NewGlobalRef(topic); // 或直接用 NewStringUTF 兩次
env->SetObjectField(obj, fid, topicCopy);
// 方案 2:Java 端定義 topic 為 String,JNI 塞入後立即在 Java 複製
// 但最佳實踐:JNI 僅填充數據,生命週期交由 Java 管理
// 正確模式:創建 Java String 對象並賦值,JVM 自動管理其生命週期
jstring topic = env->NewStringUTF("Topic 0"); // 局部引用
env->SetObjectField(obj, fid, topic); // 字段持有引用,GC 可見
// 方法返回時,局部引用表清理,但字段引用保護對象存活 → 安全
關鍵認知:SetObjectField 將引用存入 Java 堆對象字段,該字段成為 GC Root 可達路徑一部分,**對象不會被回收**。崩潰說明存入的引用本身已無效(如指向已釋放的緩衝區、棧內存、錯誤地址)。
-XX:ErrorFile=./hs_err_pid%p.log-Xcheck:jni,自動檢測常見錯誤(性能影響大,僅調試用)GetStringUTFChars 必配 ReleaseStringUTFChars;GetPrimitiveArrayCritical 必配 ReleaseNewGlobalRef,用畢 DeleteGlobalRefAttachCurrentThread / DetachCurrentThread 成對使用-Wall -Werror -fstack-protector-strong,鏈接 -Wl,-z,relro,-z,now本案例崩潰本質是 JNI 端返回了包含無效引用的對象數組,導致 Java 端訪問字段時觸發 JVM 內部訪問違規。JNI 開發容錯率極低,**「編譯通過 ≠ 正確」**,必須建立引用生命週期管理、邊界檢查、異常安全的工程化規範,並善用 -Xcheck:jni、Sanitizer 等工具在開發期暴露問題。