在某些情況下,Java語言需要通過調用C/C++函數來實現某些功能,因為Java有時候對這些功能顯的無能為力,如想使用X86_64 的 SIMD 指令提升一下業務方法中關鍵代碼的性能,又或者想要獲取某個體系架構或者操作系統特有功能的支持。為了能在Java 代碼中調用 C/C++函數,JVM提供了Java Native Interface(JNI)機制。 在Java中,使用native關鍵字標注的、沒有方法體的方法就是native方法。當在 Java 代碼中調用這些 native 方法時,Java 虛擬機將通過JNI調用到對應的 C/C++ 函數。那么普通的Java方法和native方法有什么區別呢?
native方法與java普通方法的區別:
(1)普通Java方法在解釋執行情況下,調用dispatch_next()函數執行每一條字節碼指令並達到解釋執行的效果,而本地C/C++函數會通過C/C++編譯器編譯為機器指令執行,所以Java方法可能會采用解釋執行,而C/C++函數會編譯執行;
(2)普通Java方法(包括普通Java同步方法)的入口例程是由HotSpot VM的generate_normal_entry()函數生成的,而native方法(包括native同步方法)的入口例程是由generate_native_entry()函數生成的。在對同步方法進行處理時,generate_normal_entry()函數中調用lock_method()函數生成例程,這個例程會對Java方法加鎖而沒有對應的釋放鎖邏輯,因為dispatch_next()函數執行字節碼指令時,一些字節碼如return、athrow在移除棧幀的時候會有釋放鎖的操作,所以無須生成釋放鎖的邏輯,但是generate_native_entry()函數生成的例程沒有執行字節碼指令,它必須在執行完native方法之后檢查是否需要執行釋放鎖操作。generate_native_entry()函數生成的例程到目前為止還沒有介紹,不過后面我們馬上會介紹。
我之前在開發某個性能故障排查工具時,因為這個工具需要支持不同的操作系統,所以我選擇使用Java語言開發,但是在開發過程中需要根據進程pid來獲取應用程序的執行目錄,而Java的核心庫又無法提供出這樣的功能,所以我只能借助JNI機制來開發。通過這樣的開發方式雖然能滿足一定的需求,但是不要忘記,這會犧牲可移植性,我需要在linux、Mac和Windows平台上生成各自的.so、.dylib和.dll動態鏈接庫,非常的麻煩。另外在使用JNI機制開發時,還有一些缺點,如下:
- 從 Java 環境到 native code 的上下文切換耗時、低效;
- JNI 編程的潛在風險變大,容易出現內存泄漏等問題,甚至Java 虛擬機的崩潰。
下面舉一個JNI實例,如下:
public class TestJNI { static { // 程序在加載時,自動加載libdiaoyong.so庫 System.loadLibrary("diaoyong"); } // 聲明原生函數。注意要添加native關鍵字 public native void set(int value); public native int get(); public static void main(String[] args) { TestJNI test = new TestJNI(); test.set(1); System.out.println(test.get()); } }
調用JNI的時候,通常使用System.loadLibrary()方法加載JNI library,同樣也可以使用System.load()方法加載JNI library,兩者的區別是一個只需要設置庫的名字,比如如果動態鏈接庫的名稱為libA.so,則只要輸入A就可以了,而libA.so的位置可以通過設置java.library.path或者sun.boot.library.path指定,而System.load()方法需要輸入完整路經的文件名。
下面編寫native方法對應的C/C++函數的本地實現,如下:
// 命令生成java.class文件。假設TestJNI在包com/test下,則也是在com/test下使用這個命令生成java.class文件。 javac TestJNI.java // 命令生成TestJNI.h文件。假設TestJNI在包com/test下,則要切換到com/test的上一級后 // 使用javah -jni com.test.TestJNI這個命令生成類似於com_test_TestJNI.h文件。 javah -jni TestJNI
生成的TestJNI.h文件的內容如下:
/* DO NOT EDIT THIS FILE - it is machine generated */ #include <jni.h> /* Header for class TestJNI */ #ifndef _Included_TestJNI #define _Included_TestJNI #ifdef __cplusplus extern "C" { #endif /* * Class: TestJNI * Method: set * Signature: (I)V */ JNIEXPORT void JNICALL Java_TestJNI_set(JNIEnv *, jobject, jint); /* * Class: TestJNI * Method: get * Signature: ()I */ JNIEXPORT jint JNICALL Java_TestJNI_get(JNIEnv *, jobject); #ifdef __cplusplus } #endif #endif
JNIEXPORT和JNICALL都是JNI的關鍵字,表示此函數是要被JNI調用的。jint是以JNI為中介使Java的int類型與本地的int類型溝通的一種類型。函數的名稱是Java_Java程序的package路徑_函數名組成的。
現在我們規范一下術語,如下:
除了native方法,本地函數外,還有JNI函數,這是HotSpot VM為本地函數提供的,用來訪問HotSpot VM內部服務的函數。
下表詳細介紹了Java中與C/C++中類型的對應關系。
Java | JNI中的別名 | C/C++中的類型 | 字節數 |
boolean | jboolean | unsigned char | 1 |
byte | jbyte | signed char | 1 |
char | jchar | unsigned short | 2 |
short | jshort | short | 2 |
int | jint/jsize | long | 4 |
long | jlong | __int64 | 8 |
float | jfloat | float | 4 |
double | jdouble | double | 8 |
jobject是個JNI句柄,或稱為native句柄、本地句柄。在開發native時,經常會用到這個句柄,如果native方法是個實例(非靜態)方法,生成的本地函數第2個參數類型就是jobject,用於表示該native方法所對應的Java對象的JNI句柄。
// C++使用的_jobject的定義 class _jobject {}; typedef _jobject *jobject; // C使用的_jobject的定義 struct _jobject; typedef struct _jobject *jobject;
jobject是_jobject類型的指針,我們在實際過程中可以這樣使用:
jobject handle = ... oop* ptr = (oop*)handle;
JNI句柄可以直接轉換為一個oop指針。jobject是指針類型,oop*明顯也是指針類型,不過由於oop本身就是指針類型,所以handle可以說是指針的指針。
對於數組類型的對應關系如下表所示。
Java | C/C++ |
boolean[ ] | JbooleanArray |
byte[ ] | JbyteArray |
char[ ] | JcharArray |
short[ ] | JshortArray |
int[ ] | JintArray |
long[ ] | JlongArray |
float[ ] | JfloatArray |
double[ ] | JdoubleArray |
對於本地函數來說,函數的名稱默認一般為“Java_Java程序的package路徑_函數名”組成的。
本地函數的第一個參數JNIEnv接口指針,指向一個函數表,函數表中的每一個入口指向一個JNI函數。本地函數經常通過這些函數來訪問HotSpot中的數據結構,如堆中的oop等。下圖演示了JNIEnv這個指針:
本地函數的第二個參數根據native方法是一個靜態方法還是實例方法而有所不同。本地方法是一個靜態方法時,第二個參數代表本地方法所在的類;本地方法是一個實例方法時,第二個參數代表本地方法所在的對象。如上例子的Java_TestJNI_get()函數與Java_TestJNI_set()函數是native實例方法的本地實現,因此jobject參數指向方法所在的對象。
繼續編寫對應的c語言的實現,如下:
#include <stdio.h> #include "TestJNI.h" int i=0; JNIEXPORT void JNICALL Java_TestJNI_set(JNIEnv * env, jobject obj, jint j) { i=j*888; } JNIEXPORT jint JNICALL Java_TestJNI_get(JNIEnv * env, jobject obj){ printf("ok!You have successfully passed the Java call c\n"); return i; }
對於obj來說,如果native方法不是static的話,這個obj就代表這個native方法的類實例。如果native方法是static的話,這個obj就代表這個native方法的類的Class對象(static方法不需要類實例,所以就代表這個類的Class對象)
使用如下命令生成TestJNI.o文件。
gcc -Wall -fPIC -c TestJNI.c -I ./ \ -I /home/mazhi/workspace/jdk1.8.0_192/include/linux/ \ -I /home/mazhi/workspace/jdk1.8.0_192/include/
命令中的參數解析如下:
-Wall:打開警告開關。
-fPIC:表示編譯為位置獨立的代碼,不用此選項的話編譯后的代碼是位置相關的所以動態載入時是通過代碼拷貝的方式來滿足不同進程的需要,而不能達到真正代碼段共享的目的。
gcc -Wall -rdynamic -shared -o libdiaoyong.so TestJNI.o
命令中的參數解析如下:
動態鏈接庫的名字必須是 lib*.so,因為編譯器查找動態連接庫時有隱含的命名規則,即在給出的名字前面加上lib,后面加上.so來確定庫的名稱。這里是libdiaoyong.so對應於Java程序里的diaoyong。
選項 -rdynamic 用來通知鏈接器將所有符號添加到動態符號表中。
-shared指編譯后會鏈接成共享對象。
編譯~/.bashrc文件,添加環境變量的配置export LD_LIBRARY_PATH=./ 使用source ~/.bashrc命令使配置生效。之前在TestJNI類中的如下調用:
System.loadLibrary("diaoyong");
意思就是生成的動態庫文件名為libdiaoyong.so(這是linux環境)(如果是window環境,則為diaoyong.dll)。這里可能有人就會問,這個libdiaoyong.so文件應該放在哪里呢?
這個需要放到linux系統下的JNI環境中,也就是說必須聲明一個環境變量,對應一個文件夾,然后這個文件就放在這個文件夾下面就可以找到了。
最后通過java TestJNI命令對運行Java程序后,可以看到正確的輸出結果。
JNI接口的實現 有介紹到如何對JNI進行調試 https://www.cnblogs.com/selonsy/p/15842914.html
公眾號 深入剖析Java虛擬機HotSpot 已經更新虛擬機源代碼剖析相關文章到60+,歡迎關注,如果有任何問題,可加作者微信mazhimazh,拉你入虛擬機群交流