前言:何時需要 JNI?
- 平台相關功能(硬體控制、OS API、GUI 整合)
- 高效能數學/圖像/加密運算(C/C++ SIMD、GPU)
- 既有 C/C++ 庫複用(避免重寫、驗證成本)
- Java 標準庫不支援的特殊需求
JNI 允許 Java 代碼與本地代碼(C/C++/Assembly)雙向調用,但犧牲可移植性、增加複雜度、引入崩潰風險,非必要勿用。
一、JNI 工作原理核心機制
本地方法聲明與實現對應
// Java 端
package pkg;
class Cls {
native double f(int i, String s);
static {
System.loadLibrary("pkg_Cls"); // 載入本地庫
}
}
// C++ 端 (pkg_Cls.cpp)
#include
JNIEXPORT jdouble JNICALL Java_pkg_Cls_f(JNIEnv *env, jobject obj, jint i, jstring s) {
// 實現邏輯
return 0.0;
}
命名規範
- 函數名:
Java_包名_類名_方法名(包名用 _ 分隔,_ 轉義為 _1)
- 庫名映射:Linux
libpkg_Cls.so、Windows pkg_Cls.dll、macOS libpkg_Cls.dylib
JNIEnv 介面指針本質
JNIEnv* 指向函數指針表(虛函數表模式)
- 每個執行緒擁有獨立
JNIEnv*,不可跨執行緒傳遞
- 同一執行緒多次調用本地方法,JVM 保證傳遞相同
JNIEnv*
- 提供 200+ 操作函數:物件操作、欄位/方法存取、異常拋出、字串/陣列轉換、監視器鎖等
本地庫載入時機
System.loadLibrary():依賴 java.library.path,載入單一庫
System.load():指定完整路徑
- 通常在
static {} 區塊載入,確保類初始化時就緒
二、資料類型映射完整對照表
基本類型:直接對應,無轉換開銷
| Java | JNI (C) | 位寬 | 備註 |
| boolean | jboolean | 8-bit | unsigned char,JNI_TRUE/JNI_FALSE |
| byte | jbyte | 8-bit | signed char |
| char | jchar | 16-bit | unsigned short,Unicode |
| short | jshort | 16-bit | signed short |
| int | jint | 32-bit | signed int |
| long | jlong | 64-bit | signed long long |
| float | jfloat | 32-bit | float |
| double | jdouble | 64-bit | double |
| void | void | - | 僅返回類型 |
引用類型:不透明控制碼
| Java | JNI | 說明 |
| Object | jobject | 所有物件統一類型 |
| Class | jclass | FindClass 獲取 |
| String | jstring | Unicode 字串,非 C 字串 |
| Object[] | jobjectArray | 物件陣列 |
| 基本型別陣列 | jintArray, jbyteArray... | 對應各基本類型陣列 |
| Throwable | jthrowable | 異常物件 |
三、字串與陣列轉換:最高風險區
字串處理正確模式
JNIEXPORT void JNICALL Java_Cls_method(JNIEnv *env, jobject obj, jstring javaStr) {
// 1. 獲取 UTF-8 或 Unicode 指針(必須 Release)
const char *utfChars = env->GetStringUTFChars(env, javaStr, NULL);
if (utfChars == NULL) return; // OOM
// 2. 使用 C 字串
printf("Java string: %s\n", utfChars);
// 3. 釋放(即使異常也必須釋放,建議用 goto cleanup 或 RAII)
env->ReleaseStringUTFChars(env, javaStr, utfChars);
}
關鍵陷阱
- 不可直接使用 jstring:內部為 Unicode,非以 \0 結尾
- GetStringUTFChars 可能複製:返回指針可能指向臨時緩衝區
- 必須成對呼叫 Release:否則記憶體洩漏、JVM 崩潰
- 含特殊字元(如 *)的 jstring 直接操作會崩潰:必須轉換
- 修改字串內容:Java String 不可變,JNI 修改會破壞常量池、安全性
陣列處理模式
// 基本型別陣列:GetArrayElements / ReleaseArrayElements
jintArray javaArr = ...;
jint *cArr = env->GetIntArrayElements(env, javaArr, NULL);
if (cArr) {
// 直接操作 cArr[0]...cArr[len-1]
env->ReleaseIntArrayElements(env, javaArr, cArr, 0); // mode: 0=copy back, JNI_COMMIT=commit only, JNI_ABORT=discard
}
// 物件陣列:GetObjectArrayElement / SetObjectArrayElement
jobjectArray objArr = ...;
jobject elem = env->GetObjectArrayElement(env, objArr, index);
關鍵區別
- 基本型別陣列:可能複製(需 Release 提交),也可能釘住記憶體
- 物件陣列:始終操作引用,無複製語義
- GetPrimitiveArrayCritical / ReleasePrimitiveArrayCritical:高效能版本,期間禁止阻塞/分配/GC,極度謹慎使用
四、物件操作核心 API
| 操作 | 關鍵函數 |
| 尋找類 | FindClass(env, "pkg/Cls") |
| 建立物件 | NewObject(env, clazz, methodID, args...) |
| 獲取欄位 ID | GetFieldID(env, clazz, "fieldName", "sig") |
| 讀/寫欄位 | GetField / SetField |
| 獲取方法 ID | GetMethodID / GetStaticMethodID |
| 呼叫方法 | CallMethod / CallStaticMethod |
| 拋出異常 | ThrowNew(env, clazz, "msg") |
| 全域/弱全域引用 | NewGlobalRef / NewWeakGlobalRef / DeleteGlobalRef |
簽名描述符必背
基本類型:Z B C S I J F D V
物件:Lpkg/Cls;
陣列:[I, [Lpkg/Cls;
方法:(參數簽名)返回簽名 例:(ILjava/lang/String;)V
五、實戰避坑十大鐵律
- JNIEnv 絕不跨執行緒:使用
AttachCurrentThread 獲取當前執行緒專屬 Env
- 本地引用自動釋放但有上限(通常 512),大量循環建立物件需
DeleteLocalRef 或 PushLocalFrame/PopLocalFrame
- 全域引用必須手動刪除:
DeleteGlobalRef,否則永久洩漏
- 異常檢查:每次 JNI 調用後檢查
ExceptionCheck(),或用 ExceptionOccurred() 處理
- 字串/陣列 Get/Release 成對出現:用 goto cleanup 或 C++ RAII 保證
- 不要在 JNI 中長時間阻塞:阻塞持有 JVM 內部鎖,導致 GC、執行緒調度停頓
- 載入庫失敗診斷:
java.library.path、依賴庫、架構匹配(32/64位)、符號缺失
- 效能權衡:JNI 調用開銷大(~50-100ns),細粒度調用得不償失,批量數據傳輸用陣列
- 安全性:JNI 代碼繞過 Java 安全管理器,輸入驗證、緩衝區溢出防護必須自行實現
- 替代方案優先:JNA、JNR、Panama (Foreign Function & Memory API)、gRPC/REST 微服務化
結語
JNI 是 Java 與本地世界的橋樑,威力巨大但雙刃劍。掌握 JNIEnv 執行緒親和性、引用生命週期管理、字串陣列轉換協議、異常處理規範,是寫出穩定高效本地代碼的前提。現代專案優先考慮 Project Panama (FFM API)、JNA 或 微服務化 替代傳統 JNI,在安全性、可維護性、開發效率上均有質的飛躍。