Java?JNI的高級(jí)用法示例詳解
1. native 層多線程與 JVM 交互
1.1 native 層啟動(dòng)線程
在 JNI 中,native 層可以創(chuàng)建自己的線程(如 pthread、std::thread),但這些線程不是 JVM 線程,不能直接訪問 JVM 資源。
必須 attach 到 JVM,才能安全調(diào)用 Java 對(duì)象或方法。
1.2 attach/detach 線程
JavaVM* jvm; // 全局保存 JavaVM 指針
// 線程函數(shù)
void* thread_func(void* arg) {
JNIEnv* env;
// 線程 attach 到 JVM
(*jvm)->AttachCurrentThread(jvm, (void**)&env, NULL);
// 可以安全訪問 Java 對(duì)象和方法
// ...
// 線程結(jié)束前 detach
(*jvm)->DetachCurrentThread(jvm);
return NULL;
}注意:
- attach 后才能用 JNI API。
- detach 前必須釋放所有局部引用。
- 多線程下要小心全局引用的并發(fā)安全(加鎖)。
1.3 native 層回調(diào) Java(多線程)
- 通過全局引用保存回調(diào)對(duì)象,native 線程 attach 后用 CallVoidMethod/CallStaticMethod 調(diào)用 Java 方法。
- 回調(diào)后及時(shí)刪除局部引用,防止內(nèi)存泄漏。
2. 復(fù)雜對(duì)象的 JNI 傳遞與構(gòu)造
2.1 Java 傳對(duì)象給 native
Java:
public class Person {
public int age;
public String name;
}
public native void processPerson(Person p);C:
jclass cls = (*env)->GetObjectClass(env, person); jfieldID fid_age = (*env)->GetFieldID(env, cls, "age", "I"); jint age = (*env)->GetIntField(env, person, fid_age); jfieldID fid_name = (*env)->GetFieldID(env, cls, "name", "Ljava/lang/String;"); jstring jname = (jstring)(*env)->GetObjectField(env, person, fid_name); // 轉(zhuǎn)換為 C 字符串 const char* cname = (*env)->GetStringUTFChars(env, jname, NULL); // ... (*env)->ReleaseStringUTFChars(env, jname, cname);
2.2 native 構(gòu)造 Java 對(duì)象并返回
C:
jclass cls = (*env)->FindClass(env, "Person"); jmethodID ctor = (*env)->GetMethodID(env, cls, "<init>", "()V"); jobject obj = (*env)->NewObject(env, cls, ctor); jfieldID fid_age = (*env)->GetFieldID(env, cls, "age", "I"); (*env)->SetIntField(env, obj, fid_age, 25); // 返回 obj 給 Java return obj;
2.3 復(fù)雜數(shù)組/集合的傳遞
- 對(duì)于 Java List/Map,native 層一般用反射方式訪問元素,效率較低。
- 性能敏感時(shí)建議用數(shù)組(如 int[]、Object[]),native 層用 GetObjectArrayElement 操作。
3. Java Lambda 與 JNI
3.1 Java Lambda 本質(zhì)
Lambda 是編譯期自動(dòng)生成的匿名類對(duì)象,實(shí)現(xiàn)了目標(biāo)接口(如 Runnable、Function)。
JNI 層看到的是普通的 Java 對(duì)象。
3.2 JNI 層調(diào)用 Lambda
- JNI 層可接收 lambda 作為參數(shù),只要用接口類型聲明。
- JNI 通過反射/接口調(diào)用,調(diào)用 lambda 的方法(如 run、apply)。
示例:
public native void useCallback(Runnable r);
useCallback(() -> System.out.println("Hello from Lambda!"));
C:
jclass cls = (*env)->GetObjectClass(env, runnable); jmethodID mid = (*env)->GetMethodID(env, cls, "run", "()V"); (*env)->CallVoidMethod(env, runnable, mid);
3.3 局限與注意
- Lambda 不能直接在 native 層定義或?qū)崿F(xiàn)。
- native 層只能把 lambda 當(dāng)作普通對(duì)象處理,不能享受 Java 層類型推斷等語(yǔ)法糖。
- 性能敏感場(chǎng)景下,頻繁回調(diào) lambda 會(huì)有 JNI 橋接開銷。
4. JNI 性能基準(zhǔn)測(cè)試與優(yōu)化
4.1 性能測(cè)試方法
- 使用 JMH(Java Microbenchmark Harness)寫基準(zhǔn)測(cè)試,比較 Java 方法、JNI 方法、JNA 方法的調(diào)用耗時(shí)。
- 典型測(cè)試:循環(huán)調(diào)用百萬(wàn)次,測(cè)量總耗時(shí)。
JMH 示例:
@Benchmark
public void testJavaAdd() {
int a = 1, b = 2;
int c = a + b;
}
@Benchmark
public void testJNIAdd() {
nativeAdd(1, 2);
}4.2 常見性能瓶頸
- JNI 方法調(diào)用有固定的橋接開銷(幾十到幾百納秒)。
- 數(shù)據(jù)類型轉(zhuǎn)換(如字符串、數(shù)組)開銷大。
- 頻繁創(chuàng)建/釋放引用、反射操作等會(huì)拖慢性能。
4.3 優(yōu)化建議
- 減少 JNI 調(diào)用次數(shù):能批量處理就批量,避免頻繁小操作。
- 緩存 class/methodID:避免每次都查找。
- 用直接緩沖區(qū)(DirectByteBuffer):大數(shù)據(jù)傳遞更高效。
- 避免不必要的數(shù)據(jù)拷貝:如數(shù)組可用 GetPrimitiveArrayCritical。
- 合理管理引用:用完及時(shí)釋放局部/全局引用。
- 選擇合適的傳遞方式:復(fù)雜結(jié)構(gòu)建議序列化或用 protobuf/C結(jié)構(gòu)體映射。
5. 典型面試題與實(shí)戰(zhàn)解析
native 層多線程調(diào)用 Java 方法有什么注意事項(xiàng)?
- 線程必須 attach/detach,回調(diào)對(duì)象需全局引用,注意并發(fā)安全。
如何從 native 層構(gòu)造 Java 對(duì)象并返回?
- 用 FindClass、GetMethodID、NewObject、SetXXXField。
lambda 作為回調(diào)參數(shù)傳給 native,JNI 如何調(diào)用?
- 當(dāng)作接口對(duì)象,反射調(diào)用相應(yīng)方法(如 run/apply)。
JNI 性能瓶頸主要在哪?如何優(yōu)化?
- 橋接和數(shù)據(jù)轉(zhuǎn)換開銷大,建議批量處理、緩存 methodID、用直接緩沖區(qū)。
6. native 層多線程最佳實(shí)踐
6.1 多線程安全 attach/detach
- 獲取 JavaVM 指針:在 JNI_OnLoad 里全局保存 JavaVM 指針,供所有 native 線程使用。
- 線程 attach:每個(gè) native 線程首次需要 attach 到 JVM,獲得 JNIEnv 指針。
- 線程 detach:線程退出前 detach,避免 JVM 資源泄露。
示例:
JavaVM* g_jvm = NULL;
jint JNI_OnLoad(JavaVM* vm, void* reserved) {
g_jvm = vm;
return JNI_VERSION_1_6;
}
void* worker(void* arg) {
JNIEnv* env;
if ((*g_jvm)->AttachCurrentThread(g_jvm, (void**)&env, NULL) == 0) {
// 使用 env 調(diào)用 Java 方法
// ...
(*g_jvm)->DetachCurrentThread(g_jvm);
}
return NULL;
}6.2 并發(fā)安全的回調(diào)
- 回調(diào)對(duì)象需用 NewGlobalRef 創(chuàng)建全局引用,避免被 GC 回收。
- 回調(diào)時(shí)加鎖(如 pthread_mutex),確保多線程安全。
- 回調(diào)后及時(shí) DeleteGlobalRef,避免內(nèi)存泄漏。
7. 復(fù)雜對(duì)象高效傳遞與映射
7.1 批量數(shù)據(jù)結(jié)構(gòu)傳遞
- 對(duì)于大量數(shù)據(jù)(如點(diǎn)、向量、結(jié)構(gòu)體),建議用 ByteBuffer 或直接數(shù)組傳遞,native 層按內(nèi)存結(jié)構(gòu)解析。
- 可用 Java 的 DirectByteBuffer,native 層用 GetDirectBufferAddress 獲得指針,零拷貝高性能。
Java:
ByteBuffer buf = ByteBuffer.allocateDirect(1024); nativeProcessBuffer(buf);
C:
void JNICALL nativeProcessBuffer(JNIEnv* env, jobject obj, jobject buffer) {
void* ptr = (*env)->GetDirectBufferAddress(env, buffer);
// 直接操作內(nèi)存
}7.2 復(fù)雜對(duì)象序列化
- 對(duì)于復(fù)雜 Java 對(duì)象(如 Map/List),可以用 JSON、protobuf、flatbuffers 序列化后傳遞到 native 層,native 層反序列化為 C 結(jié)構(gòu)體。
- 這樣可以避免 JNI 的反射遍歷,提高性能和靈活性。
8. lambda 在高性能場(chǎng)景下的實(shí)戰(zhàn)建議
8.1 場(chǎng)景分析
- Java 層傳遞 lambda 作為回調(diào),native 層保存并在事件發(fā)生時(shí)回調(diào)。
- 性能敏感時(shí),建議只傳遞接口對(duì)象,避免頻繁回調(diào)和反射。
8.2 高效回調(diào)設(shè)計(jì)
- native 層只保存接口對(duì)象的全局引用,不做多余的類型檢查。
- 回調(diào)時(shí)直接用 methodID 調(diào)用,不用每次查找。
- 如果回調(diào)次數(shù)極多,可設(shè)計(jì)事件隊(duì)列,由 Java 層統(tǒng)一處理,減少 JNI 交互頻率。
9. JNI 性能基準(zhǔn)測(cè)試與分析
9.1 JMH 基準(zhǔn)測(cè)試示例
@Benchmark
public void javaMethod() {
// 普通 Java 方法
}
@Benchmark
public void jniMethod() {
nativeMethod();
}
@Benchmark
public void jniArrayMethod() {
nativeArrayMethod(new int[1000]);
}測(cè)試結(jié)論:
- 普通 Java 方法最快。
- JNI 方法有橋接開銷,單次調(diào)用比 Java 慢幾十到幾百納秒。
- 傳遞數(shù)組/對(duì)象時(shí),JNI 開銷更大,建議批量處理。
9.2 優(yōu)化建議
- 批量處理:如 1000 個(gè)點(diǎn)一次傳遞,避免 1000 次 JNI 調(diào)用。
- 緩存 class/methodID:只查找一次,后續(xù)直接用。
- DirectByteBuffer/序列化:大數(shù)據(jù)零拷貝或高效解析。
- 避免反射和頻繁創(chuàng)建引用。
10. JNI 在實(shí)際項(xiàng)目中的架構(gòu)建議
10.1 封裝 native 層 API
- Java 層只暴露簡(jiǎn)單的 native 方法,參數(shù)類型盡量基礎(chǔ)(如數(shù)組、ByteBuffer)。
- native 層用 C/C++ 封裝復(fù)雜邏輯,統(tǒng)一管理線程和資源,避免 Java 層直接暴露復(fù)雜對(duì)象。
10.2 資源和異常管理
- 所有 native 層分配的內(nèi)存和引用都需及時(shí)釋放。
- native 層出錯(cuò)時(shí)及時(shí) ThrowNew Java 異常,不讓 JVM 崩潰。
10.3 跨平臺(tái)適配
- 動(dòng)態(tài)庫(kù)需分別編譯 Windows/Linux/macOS,接口保證一致性。
- 可用 CMake/autotools 管理多平臺(tái)構(gòu)建。
11. 典型面試題解析(補(bǔ)充)
如何用 JNI 實(shí)現(xiàn) Java 層的事件回調(diào)?
- 保存回調(diào)對(duì)象的全局引用,native 事件發(fā)生時(shí) attach 線程并調(diào)用 Java 方法。
批量數(shù)據(jù)高效傳遞的最佳實(shí)踐?
- 用 DirectByteBuffer 或 protobuf 序列化,native 層直接解析內(nèi)存。
native 多線程并發(fā)安全的關(guān)鍵點(diǎn)?
- attach/detach 線程,回調(diào)對(duì)象用全局引用,操作時(shí)加鎖。
JNI 性能優(yōu)化的核心原則?
- 減少調(diào)用次數(shù),批量處理,緩存查找結(jié)果,零拷貝傳遞。
11. 總結(jié)
- JNI 支持 native 層多線程、復(fù)雜對(duì)象交互、Java lambda 回調(diào)等高級(jí)用法,但需嚴(yán)格管理線程、引用和資源。
- 性能優(yōu)化需關(guān)注調(diào)用次數(shù)、數(shù)據(jù)傳遞方式和引用管理。
- 工程實(shí)踐中建議只在必要場(chǎng)景使用 JNI,盡量封裝好接口,簡(jiǎn)化 Java 與 native 的交互。
- JNI 高級(jí)用法需關(guān)注多線程安全、復(fù)雜對(duì)象高效傳遞、回調(diào)機(jī)制、性能基準(zhǔn)與優(yōu)化。
- 工程實(shí)踐中建議封裝好接口,統(tǒng)一管理資源和異常,保證跨平臺(tái)一致性和高性能。
- 面試和項(xiàng)目中,能用 DirectByteBuffer、序列化、全局引用等技巧,往往體現(xiàn)高級(jí)水平。
到此這篇關(guān)于Java JNI的高級(jí)用法的文章就介紹到這了,更多相關(guān)Java JNI高級(jí)用法內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
詳解Java數(shù)據(jù)結(jié)構(gòu)和算法(有序數(shù)組和二分查找)
本篇文章主要介紹了詳解Java數(shù)據(jù)結(jié)構(gòu)和算法(有序數(shù)組和二分查找),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2017-09-09
Mybatis-Plus 條件構(gòu)造器 QueryWrapper 的基本用法
這篇文章主要介紹了Mybatis-Plus - 條件構(gòu)造器 QueryWrapper 的使用,通過實(shí)例代碼給大家介紹了查詢示例代碼及實(shí)現(xiàn)需求,代碼簡(jiǎn)單易懂,對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2021-09-09
MyBatis 動(dòng)態(tài)SQL之where標(biāo)簽的使用
本文主要介紹了MyBatis 動(dòng)態(tài)SQL之where標(biāo)簽,where 標(biāo)簽主要用來(lái)簡(jiǎn)化 SQL 語(yǔ)句中的條件判斷,可以自動(dòng)處理 AND/OR 條件,下面就來(lái)具體介紹一下2024-01-01
SpringBoot 項(xiàng)目中的圖片處理策略之本地存儲(chǔ)與路徑映射
在SpringBoot項(xiàng)目中,靜態(tài)資源存放在static目錄下,使得前端可以通過URL來(lái)訪問這些資源,我們就需要將文件系統(tǒng)的文件路徑與URL建立一個(gè)映射關(guān)系,把文件系統(tǒng)中的文件當(dāng)成我們的靜態(tài)資源即可,本文給大家介紹SpringBoot本地存儲(chǔ)與路徑映射的相關(guān)知識(shí),感興趣的朋友一起看看吧2023-12-12

