首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
您现在的位置: 程式碼之家 >> 編程語言 >  >> Java 程式設計 >> Java 相關知識

JNI 調用導致 JVM 崩溃 (EXCEPTION_ACCESS_VIOLATION) 根因分析與排查指南

     編輯:Java 相關知識

JNI (Java Native Interface) 調用是 Java 與 C/C++ 交互的橋樑,但也是 JVM 崩溃的高發區。本文以實際 EXCEPTION_ACCESS_VIOLATION (0xc0000005) 崩潰日誌為例,深度解析崩潰堆疊、定位.native 代碼缺陷、提供系統化排查清單與避坑指南。

一、 崩潰現場還原

Java 代碼

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

二、 堆疊深度解讀

  1. 觸發點:System.out.println(obj.getTopic()) → 字符串獲取字符 → System.arraycopy → JVM 內部訪問違規。
  2. 異常碼:0xc0000005 = 讀取非法內存地址 (0x00000009),典型的空指針解引用或對象已被回收/損壞。
  3. 關鍵線索:崩潰發生在 Java 端打印返回的 JNI 對象字段時,說明 ObjectMethod 返回的 TestJNI[] 數組或其元素內部指針無效。

三、 JNI 端常見致命錯誤模式

錯誤類型典型代碼缺陷後果
局部引用越界/釋放後使用未調用 NewGlobalRef 就長期持有 jobject;DeleteLocalRef 後繼續用指針懸空,訪問已回收內存
對象創建失敗未檢查FindClass/NewObject 返回 NULL 直接使用空指針解引用
字段/方法 ID 緩存失效類卸載後重載,舊 jfieldID/jmethodID 失效訪問錯誤偏移量
數組操作邊界錯誤SetObjectArrayElement 索引越界;GetArrayLength 未檢查內存越界寫入/讀取
字符串編碼錯誤NewStringUTF 處理非修正 UTF-8;GetStringUTFChars 未 Release內存洩漏、字符串截斷、崩潰
異常拋出未處理JNI 代碼拋出異常後未 ExceptionClear 繼續執行異常傳播到 JVM 內部導致崩潰
線程附著/分離錯誤Native 線程未 AttachCurrentThread 直接調用 JNIJNIEnv 無效,隨機崩潰

四、 針對本案例的推斷與驗證

推斷:ObjectMethod 返回的 TestJNI[] 中的元素,其 topic 字段指向的 jstring 是局部引用,在 native 方法返回前已被 JVM 自動釋放,Java 端拿到的是野指針。

錯誤 C++ 代碼示例

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 可達路徑一部分,**對象不會被回收**。崩潰說明存入的引用本身已無效(如指向已釋放的緩衝區、棧內存、錯誤地址)。

五、 系統化排查清單

  1. 獲取 hs_err_pid.log:啟動參數添加 -XX:ErrorFile=./hs_err_pid%p.log
  2. 定位 Problematic frame:V=JVM 內部、J=JIT 編譯代碼、j=解釋執行、C=Native 代碼
  3. 對照 Java frames:找到調用 native 方法的 Java 代碼行
  4. 審查對應 C/C++ 代碼:重點檢查引用管理、邊界檢查、異常處理、線程附著
  5. 啟用 JNI 校驗:啟動參數 -Xcheck:jni,自動檢測常見錯誤(性能影響大,僅調試用)
  6. 使用工具:AddressSanitizer (ASan)、Valgrind、Visual Studio Debugger 附加 java 進程調試 native 代碼

六、 JNI 開發鐵律

  1. ✅ 所有 JNI 函數返回值必須檢查 NULL
  2. ✅ GetStringUTFChars 必配 ReleaseStringUTFChars;GetPrimitiveArrayCritical 必配 Release
  3. ✅ 長期持有對象 → NewGlobalRef,用畢 DeleteGlobalRef
  4. ✅ 異常拋出後 立即 return,不再執行後續 JNI 調用
  5. ✅ 多線程環境:AttachCurrentThread / DetachCurrentThread 成對使用
  6. ✅ 編譯開啟 -Wall -Werror -fstack-protector-strong,鏈接 -Wl,-z,relro,-z,now
  7. ✅ 單元測試覆蓋 JNI 邊界條件(空輸入、極長字符串、並發調用)

七、 總結

本案例崩潰本質是 JNI 端返回了包含無效引用的對象數組,導致 Java 端訪問字段時觸發 JVM 內部訪問違規。JNI 開發容錯率極低,**「編譯通過 ≠ 正確」**,必須建立引用生命週期管理、邊界檢查、異常安全的工程化規範,並善用 -Xcheck:jni、Sanitizer 等工具在開發期暴露問題。

  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式碼之家 All Rights Reserved