| 字段 | 含義 | 本例數值 |
| Exception | 異常類型 | EXCEPTION_ACCESS_VIOLATION (0xc0000005) - 內存訪問違規 |
| pc | 程序計數器 (崩潰指令地址) | 0x6d91c1db |
| Problematic frame | 崩潰幀類型 | V = VM 內部代碼 (jvm.dll) |
| siginfo | 信號詳細信息 | reading address 0x00000009 (嘗試讀取接近 NULL 的地址) |
| Java frames | Java 層調用棧 | System.arraycopy → String.getChars → BufferedWriter.write → PrintStream.println → JNITransObject.main |
核心線索
- Java 棧顯示:崩潰發生在
PrintStream.println → String.getChars → System.arraycopy,即**字符串輸出操作**
- 讀取地址 0x9:極接近 NULL (0x0),典型**空指針解引用**
- 觸發點:
JNITransObject.main 第 29 行附近
根因分析:為什麼在 Java 打印時崩潰?
崩潰堆棧在 Java 端,但**根因在先前執行的 JNI 代碼**已破壞了 JVM 內部狀態:
最可能的 JNI 錯誤類型
- JNI 方法簽名不匹配:返回類型聲明為
TestJNI[],但 native 代碼返回錯誤類型或未正確構造數組
- 局部引用洩漏/過期引用使用:native 代碼創建對象後未正確管理局部/全局引用,導致 GC 移動對象後引用失效
- native 端 printf/cout 破壞標準輸出流:C++
std::cout 或 C printf 直接操作文件描述符 1,與 Java System.out 共享底層句柄,導致流狀態不一致
- 釋放已釋放/未分配內存:
free() 重複釋放、訪問已 DeleteLocalRef 的對象
- 調用已銷毀對象方法:對象在 native 端被釋放,Java 端仍持有引用並調用方法
排查步驟實戰
1. 獲取 hs_err_pid.log 完整文件
崩潰時 JVM 在工作目錄生成 hs_err_pid<pid>.log,包含完整寄存器、棧內存、模塊列表、環境變量。
2. 定位 native 代碼對應行
- 編譯 native 代碼時生成符號表 (
-g)
- 用
addr2line -e JNITransObject.dll 0xoffset 映射地址到源碼行
- 或 Visual Studio / WinDbg 加載 dump 分析
3. 檢查 JNI 關鍵點
// ✅ 正確:檢查異常
jobjectArray result = (*env)->NewObjectArray(env, count, cls, NULL);
if ((*env)->ExceptionCheck(env)) return NULL;
// ✅ 正確:構造對象數組
for (int i = 0; i < count; i++) {
jobject obj = (*env)->NewObject(env, cls, constructor, ...);
(*env)->SetObjectArrayElement(env, result, i, obj);
// 必須檢查每次 JNI 調用後的異常
}
// ❌ 錯誤:直接操作標準輸出
printf("debug: %s\n", str); // 破壞 System.out 內部緩衝區
// ✅ 正確:用 Java 端打印或 JNI 回調 Java 方法
jclass sysClass = (*env)->FindClass(env, "java/lang/System");
jfieldID outField = (*env)->GetStaticFieldID(env, sysClass, "out", "Ljava/io/PrintStream;");
jobject out = (*env)->GetStaticObjectField(env, sysClass, outField);
jclass psClass = (*env)->GetObjectClass(env, out);
jmethodID println = (*env)->GetMethodID(env, psClass, "println", "(Ljava/lang/String;)V");
(*env)->CallVoidMethod(env, out, println, (*env)->NewStringUTF(env, "debug msg"));
4. 啟用 JNI 關鍵檢查
# 啟動參數添加
-Xcheck:jni
# 檢查:參數類型、引用有效性、數組邊界、修飾符訪問、關鍵區域嵌套
# 會顯著降低性能,僅用於開發/測試環境
防禦性 JNI 編程規範
- 所有 JNI 調用後檢查 ExceptionCheck(),拋出 Java 異常並提前返回
- 嚴格區分局部/全局引用:
NewGlobalRef 跨調用保持,DeleteGlobalRef 及時釋放
- 禁止 native 端直接使用 stdout/stderr,統一回調 Java 打印方法
- 數組操作用 Get/Release 橋接:
GetIntArrayElements / ReleaseIntArrayElements,避免直接操作堆內存
- 關鍵區域:
GetPrimitiveArrayCritical / ReleasePrimitiveArrayCritical 內**禁止調用任何 JNI 函數、分配內存、阻塞調用**
- 編譯開啟:
-Wall -Wextra -Werror -fno-omit-frame-pointer
- 靜態分析:Clang Static Analyzer、Cppcheck 掃描 JNI 代碼
調試工具鏈
| 工具 | 用途 |
| hs_err_pid.log | 崩潰現場第一現場 |
| -Xcheck:jni | 運行時 JNI 規範檢查 |
| Visual Studio Debugger | Windows 下混合模式調試 (Java + C++) |
| WinDbg + SOS | 生產環境 Dump 事後分析 |
| Java Flight Recorder (JFR) | JDK 11+ 事件記錄,含 JNI 調用 |
| Valgrind / AddressSanitizer | Linux 下內存錯誤檢測 (需編譯插桩) |
總結
EXCEPTION_ACCESS_VIOLATION 在 JNI 調用中 90%+ 由 **native 代碼破壞 JVM 內部狀態** 引起,而非 JVM 自身 Bug。核心排查路徑:hs_err 日誌 → Java 棧定位觸發操作 → 檢查先前 JNI 代碼的引用管理、輸出流干擾、簽名匹配。落地 -Xcheck:jni、禁止 native 直接輸出、嚴格引用生命周期管理,可消絕大多數此類崩潰。
- 上一篇文章: Java 文件路徑解析:絕對/相對路徑、Web 根目錄、Classpath 獲取全攻略
- 下一篇文章: JTable 排序功能完整實現:TableSorter 源碼深度解析與使用指南
Copyright ©
程式碼之家 All Rights Reserved