java是如何呼叫native方法?hotspot原始碼分析必會技能
在學習JDK原始碼(concurrent併發包、Thread相關原始碼等)時,一層一層進入方法中,看到最底層通常都會看到一個native修飾的方法。
為什麼到看JDK原始碼時,到native方法就沒有了?native方法是幹啥的?在哪裡能看到native方法?java是如何呼叫native方法的?今天,就通過實際模擬,看看java是如何呼叫native方法的。
為了做這個測試,花了我兩個晚上,遇到各種問題。為了解決這些問題,都不知道抽了多少根菸,掉了多少的頭髮。
上正文。
一、為什麼會有native方法
java是偏上層的計算機語言,最終都需要在底層的作業系統上執行,而java是不能直接操作作業系統的。這就需要在java和作業系統之間,有一種類似語言轉義的過程。
我們知道,C語言和C++語言可以和作業系統直接互動。JDK中native方法,可以將java操作指令轉換成C和C++,從而實現和底層的作業系統互動。而將java操作轉換成C和C++的過程就是JVM完成的,jvm(比如hotspot)的原始碼中有大量的C和C++的程式碼,這些程式碼就包含JDK中native方法的具體實現了。
這裡想複習一下JDK、JRE、JVM之間的關係。JDK是Java開發工具包,是整個Java的核心,包括了Java執行環境JRE、Java工具和Java基礎類庫。JRE是JDK專案的一部分,是java的執行環境,包含JVM標準實現及Java核心類庫。JVM是java虛擬機器,是整個java實現跨平臺的最核心的部分,能夠執行以Java語言寫作的軟體程式。因此,JVM是連線java語言和作業系統的橋樑,java的”一次編譯到處執行“,就是JVM遮蔽了不同作業系統的差異,因為在JVM模組中,同一個native方法會有不同的作業系統的實現,以滿足不同作業系統的要求。因此,想了解native方法的具體實現,必須看JVM的程式碼。JVM的原始碼在哪裡?當然在JDK的原始碼當中了。這裡可以在檢視不同版本的OpenJdk的程式碼,openJdk內部就有不同版本的hotspot的實現了。
今天的重點不是JDK的原始碼,這裡就不細說了。
模擬Java呼叫c或c++寫的native方法的技術叫做JNI(Java Native Interface)。JNI可以確保程式碼在不同的平臺上方便的移植。
二、寫一個簡單的java物件
這裡寫一個簡單的java類,使用javac編譯、javap生產標頭檔案、並使用java命令執行。
/**
* Description: java呼叫C
* java方法中有很多native方法,這些方法都是hotspot中用C或者C++實現的。
* 下面模擬一個java呼叫C的過程
* @author 諸葛小猿
* @date 2020-11-11
*/
public class JavaCallC {
static {
// 使用檔名載入自定義的C語言庫
System.load("/root/java-learn/libJavaCallC.so" );
}
public static void main(String[] args) {
JavaCallC javaCallC =new JavaCallC();
// 呼叫本地方法
javaCallC.cMethod();
}
// 使用C語言實現本地方法
private native void cMethod();
}
幾個坑:
-
為了後面不會出現各種么蛾子,建議不要加包名。
-
程式碼的第12行的庫檔案,後面會生成,注意檔案的名字和路徑。庫檔案也可以使用
System.loadLibrary( "JavaCallC" )
方式載入,這種方式載入要注意庫的名字; -
程式碼的第24行,定義一個native方法。後面會使用c語言模擬實現。
三、獲得JavaCallC.class檔案
將上面的檔案上傳到Centos上,使用如下命令進行編譯。
檔案上傳路徑: /root/java-learn
在該路徑下執行編譯命令: java JavaCallC.java
該路徑下會生成一個class檔案:JavaCallC.class
四、獲得JavaCallC.h檔案
在 /root/java-learn
路徑下,使用javah命令生成標頭檔案
在該路徑下執行: javah JavaCallC
。注意不要帶字尾。
會在該路徑下生成標頭檔案:JavaCallC.h
上面的執行過程:
[root@iZuf61pdvb2o7cf4mu9ccyZ java-learn]#
[root@iZuf61pdvb2o7cf4mu9ccyZ java-learn]# pwd
/root/java-learn
[root@iZuf61pdvb2o7cf4mu9ccyZ java-learn]# ll
total 4
-rw-r--r-- 1 root root 635 Nov 12 23:45 JavaCallC.java
[root@iZuf61pdvb2o7cf4mu9ccyZ java-learn]# javac JavaCallC.java
[root@iZuf61pdvb2o7cf4mu9ccyZ java-learn]#
[root@iZuf61pdvb2o7cf4mu9ccyZ java-learn]# ll
total 8
-rw-r--r-- 1 root root 476 Nov 12 23:46 JavaCallC.class
-rw-r--r-- 1 root root 635 Nov 12 23:45 JavaCallC.java
[root@iZuf61pdvb2o7cf4mu9ccyZ java-learn]#
[root@iZuf61pdvb2o7cf4mu9ccyZ java-learn]# javah JavaCallC
[root@iZuf61pdvb2o7cf4mu9ccyZ java-learn]#
[root@iZuf61pdvb2o7cf4mu9ccyZ java-learn]# ll
total 12
-rw-r--r-- 1 root root 476 Nov 12 23:46 JavaCallC.class
-rw-r--r-- 1 root root 376 Nov 12 23:46 JavaCallC.h
-rw-r--r-- 1 root root 635 Nov 12 23:45 JavaCallC.java
[root@iZuf61pdvb2o7cf4mu9ccyZ java-learn]#
開啟標頭檔案,檢視具體內容:
[root@iZuf61pdvb2o7cf4mu9ccyZ java-learn]# cat JavaCallC.h
/* DO NOT EDIT THIS FILE - it is machine generated */
#include <jni.h>
/* Header for class JavaCallC */
#ifndef _Included_JavaCallC
#define _Included_JavaCallC
#ifdef __cplusplus
extern "C" {
#endif
/*
* Class: JavaCallC
* Method: cMethod
* Signature: ()V
*/
JNIEXPORT void JNICALL Java_JavaCallC_cMethod # 這裡就是java檔案中cMethod方法的簽名。
(JNIEnv *, jobject);
#ifdef __cplusplus
}
#endif
#endif
[root@iZuf61pdvb2o7cf4mu9ccyZ java-learn]#
標頭檔案的第16-17行很關鍵,他是上面java檔案的cMethod方法的簽名。在下面C語言實現這個方法時,方法的簽名必須和這個方法一致。
五、使用C語言模擬一個native方法
模擬一個c程式碼,檔名稱Cclass.c
:
#include <stdio.h> //標頭檔案
#include "JavaCallC.h" // java檔案頭,這裡一定要加上上面java語言的標頭檔案
// 這就是上面標頭檔案中的cMethod方法的具體實現,注意方法簽名不能變,一定要和標頭檔案一樣。
JNIEXPORT void JNICALL Java_JavaCallC_cMethod(JNIEnv *env, jobject c1)
{
// 如果java呼叫cMethod方法成功,則會列印這句話
printf("Java_JavaCallC_cMethod call succ \n");
}
// 以下所有的內容的內容是測試Cclass.c的語法的,可以省掉。
// 先宣告 後呼叫
void test(){ printf("main C \n");}
//main方法,程式入口,用於測試
int main(){ test();}
同樣將Cclass.c
上傳到Centos上,檔案上傳路徑: /root/java-learn
。
下面使用Cclass.c
生成動態連結庫檔案:libJavaCallC.so
。
[root@iZuf61pdvb2o7cf4mu9ccyZ java-learn]# gcc -fPIC -I /opt/jdk1.8.0_211/include -I /opt/jdk1.8.0_211/include/linux -shared -o libJavaCallC.so Cclass.c
很多坑:
- 生成的庫檔名字及路徑一定要和上面java檔案中載入的一致。其中
-o libJavaCallC.so
就是生成的庫檔名字。如果使用使用的是System.loadLibrary()
方式載入的庫檔案,則使用的庫名稱是: “JavaCallC”,而不是 "libJavaCallC"或 “libJavaCallC.so”。 JavaCallC.java
檔案中的native方法cMethod()在Cclass.c
檔案中的實現時,一定要和JavaCallC.h
標頭檔案中cMethed()的簽名一致,一定要使用JNIEXPORT void JNICALL Java_JavaCallC_cMethod(JNIEnv *env, jobject c1)
。Cclass.c
中一定要在檔案頭中使用#include "JavaCallC.h"
將標頭檔案包含進來,不然編譯和執行時找不到Java_JavaCallC_cMethod
。- 使用gcc編譯時,因為
Cclass.c
中包含JavaCallC.h
標頭檔案,而JavaCallC.h
標頭檔案的第二行又包含#include <jni.h>
標頭檔案,而jni.h
中又包含其他的標頭檔案,gcc編譯時,這些標頭檔案的位置要指定。這些標頭檔案都在jdk所在的目錄中,這些目錄的位置要使用引數-I
進行指定。
執行後生成共享庫(動態連結庫)檔案:libJavaCallC.so
。
編譯完成後,共享庫檔案所在的目錄加入到庫檔案的環境變數 LD_LIBRARY_PATH
中。 LD_LIBRARY_PATH
是Linux環境變數名,該環境變數主要用於指定查詢共享庫(動態連結庫)時除了預設路徑之外的其他路徑。
[root@iZuf61pdvb2o7cf4mu9ccyZ java-learn]# export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:/root/java-learn
六、執行java
通過上面的操作,在/root/java-learn
目錄下就會有如下的5個檔案:
[root@iZuf61pdvb2o7cf4mu9ccyZ java-learn]# ll
total 24
-rw-r--r-- 1 root root 594 Nov 12 22:39 Cclass.c
-rw-r--r-- 1 root root 852 Nov 12 22:05 JavaCallC.class
-rw-r--r-- 1 root root 376 Nov 12 22:05 JavaCallC.h
-rw-r--r-- 1 root root 1108 Nov 12 22:04 JavaCallC.java
-rwxr-xr-x 1 root root 6179 Nov 12 22:39 libJavaCallC.so
[root@iZuf61pdvb2o7cf4mu9ccyZ java-learn]#
下面使用java JavaCallC
命令在當前目錄下執行我們的java程式:
[root@iZuf61pdvb2o7cf4mu9ccyZ java-learn]#
[root@iZuf61pdvb2o7cf4mu9ccyZ java-learn]# java JavaCallC
Java_JavaCallC_cMethod call succ
[root@iZuf61pdvb2o7cf4mu9ccyZ java-learn]#
通過執行列印的結果Java_JavaCallC_cMethod call succ
可以看出,java呼叫到了native方法,並執行了C檔案中的方法體,並列印出執行成功。
七、遇到的問題
在做這個測試時,遇到了各種問題。這裡列出來:
[root@iZuf61pdvb2o7cf4mu9ccyZ java-learn]# java com.wuxiaolong.LB.Demo.Lesson1.JavaCallC
Error: Could not find or load main class com.wuxiaolong.LB.Demo.Lesson1.JavaCallC
[root@iZuf61pdvb2o7cf4mu9ccyZ java-learn]#
## 這個問題是因為最開始使用了包名,執行時報錯,可以通過相關的配置解決,測試中我去掉了包名。
[root@iZuf61pdvb2o7cf4mu9ccyZ java-learn]# java JavaCallC
Exception in thread "main" java.lang.UnsatisfiedLinkError: no JavaCallC in java.library.path
at java.lang.ClassLoader.loadLibrary(ClassLoader.java:1867)
at java.lang.Runtime.loadLibrary0(Runtime.java:870)
at java.lang.System.loadLibrary(System.java:1122)
at JavaCallC.<clinit>(JavaCallC.java:16)
[root@iZuf61pdvb2o7cf4mu9ccyZ java-learn]#
## 這是因為載入時使用的時System.loadLibrary(),而庫名寫錯了
[root@iZuf61pdvb2o7cf4mu9ccyZ java-learn]# gcc -fPIC -I /opt/jdk1.8.0_211/include -I /opt/jdk1.8.0_211/include/linux -shared -o libJavaCallC.so Cclass.c
Cclass.c:2:53: error: Java_JavaCallC_cMethod.h: No such file or directory
[root@iZuf61pdvb2o7cf4mu9ccyZ java-learn]#
## 這好像是因為Cclass.c檔案中沒有使用: #include "JavaCallC.h"
[root@iZuf61pdvb2o7cf4mu9ccyZ java-learn]# java JavaCallC
Exception in thread "main" java.lang.UnsatisfiedLinkError: JavaCallC.cMethod()V
at JavaCallC.cMethod(Native Method)
at JavaCallC.main(JavaCallC.java:25)
[root@iZuf61pdvb2o7cf4mu9ccyZ java-learn]#
## 這是因為Cclass.c檔案方法的簽名和JavaCallC.h標頭檔案中的不一致
[root@iZuf61pdvb2o7cf4mu9ccyZ java-learn]# gcc -fPIC -I /opt/jdk1.8.0_211/include -shared -o libJavaCallC.so Cclass.c
In file included from JavaCallC.h:2,
from Cclass.c:2:
/opt/jdk1.8.0_211/include/jni.h:45:20: error: jni_md.h: No such file or directory
In file included from JavaCallC.h:2,
from Cclass.c:2:
/opt/jdk1.8.0_211/include/jni.h:63: error: expected ‘=’, ‘,’, ‘;’, ‘asm’ or ‘__attribute__’ before ‘jsize’
/opt/jdk1.8.0_211/include/jni.h:122: error: expected specifier-qualifier-list before ‘jbyte’
/opt/jdk1.8.0_211/include/jni.h:220: error: expected specifier-qualifier-list before ‘jint’
/opt/jdk1.8.0_211/include/jni.h:1869: error: expected specifier-qualifier-list before ‘jint’
/opt/jdk1.8.0_211/include/jni.h:1877: error: expected specifier-qualifier-list before ‘jint’
/opt/jdk1.8.0_211/include/jni.h:1895: error: expected specifier-qualifier-list before ‘jint’
/opt/jdk1.8.0_211/include/jni.h:1934: error: expected ‘=’, ‘,’, ‘;’, ‘asm’ or ‘__attribute__’ before ‘jint’
/opt/jdk1.8.0_211/include/jni.h:1937: error: expected ‘=’, ‘,’, ‘;’, ‘asm’ or ‘__attribute__’ before ‘jint’
/opt/jdk1.8.0_211/include/jni.h:1940: error: expected ‘=’, ‘,’, ‘;’, ‘asm’ or ‘__attribute__’ before ‘jint’
/opt/jdk1.8.0_211/include/jni.h:1944: error: expected ‘=’, ‘,’, ‘;’, ‘asm’ or ‘__attribute__’ before ‘jint’
/opt/jdk1.8.0_211/include/jni.h:1947: error: expected ‘=’, ‘,’, ‘;’, ‘asm’ or ‘__attribute__’ before ‘void’
In file included from Cclass.c:2:
JavaCallC.h:15: error: expected ‘=’, ‘,’, ‘;’, ‘asm’ or ‘__attribute__’ before ‘void’
Cclass.c:11: error: expected ‘=’, ‘,’, ‘;’, ‘asm’ or ‘__attribute__’ before ‘void’
[root@iZuf61pdvb2o7cf4mu9ccyZ java-learn]#
## 這是因為編譯時少了引數 : -I /opt/jdk1.8.0_211/include/linux
[root@iZuf61pdvb2o7cf4mu9ccyZ java-learn]# gcc -fPIC -I /opt/jdk1.8.0_211/include -I /opt/jdk1.8.0_211/include/linux -shared -o libJavaCallC.so Cclass.c
Cclass.c:19: error: expected ‘=’, ‘,’, ‘;’, ‘asm’ or ‘__attribute__’ before ‘void’
[root@iZuf61pdvb2o7cf4mu9ccyZ java-learn]#
## 這好像是因為Cclass.c檔案方法的簽名和JavaCallC.h標頭檔案中的不一致
[root@iZuf61pdvb2o7cf4mu9ccyZ java-learn]# gcc -fPIC -I /opt/jdk1.8.0_211/include -I /opt/jdk1.8.0_211/include/linux -shared -o libJavaCallC.so Cclass.c
Cclass.c: In function ‘Java_JavaCallC_cMethod’:
Cclass.c:12: error: expected declaration specifiers before ‘printf’
Cclass.c:13: error: expected declaration specifiers before ‘}’ token
Cclass.c:13: error: expected ‘{’ at end of input
[root@iZuf61pdvb2o7cf4mu9ccyZ java-learn]#
## 這好像是因為Cclass.c檔案方法的簽名和JavaCallC.h標頭檔案中的不一致
關注公眾號,輸入“java-summary”即可獲得原始碼。
完成,收工!
【傳播知識,共享價值】,感謝小夥伴們的關注和支援,我是【諸葛小猿】,一個彷徨中奮鬥的網際網路民工。
相關文章
- Java1.7的HashMap原始碼分析-面試必備技能JavaHashMap原始碼面試
- React Native 0.55.4 Android 原始碼分析(Java層原始碼解析)React NativeAndroid原始碼Java
- JavaScript如何呼叫Native iOS/Android 方法JavaScriptiOSAndroid
- Java 原始碼如何分析?Java原始碼
- Java 進階必備:HashMap 原始碼分析JavaHashMap原始碼
- Mybatis原始碼分析(四)mapper介面方法是怎樣被呼叫到的MyBatis原始碼APP
- React Native BackHandler exitApp 原始碼分析React NativeAPP原始碼
- 死磕以太坊原始碼分析之EVM如何呼叫ABI編碼的外部方法原始碼
- 從HotSpot原始碼理解DirectByteBufferHotSpot原始碼
- WindowManager呼叫流程原始碼分析原始碼
- 如何讀open jdk native 原始碼JDK原始碼
- 聊聊HotSpot VM的Native Memory TrackingHotSpot
- SOFA 原始碼分析 — 泛化呼叫原始碼
- React Native通訊原理原始碼分析一React Native原始碼
- React Native通訊原理原始碼分析二React Native原始碼
- 面試必會之ArrayList原始碼分析以及手寫ArrayList面試原始碼
- 【Java】NIO中Selector的select方法原始碼分析Java原始碼
- Dubbo原始碼分析(十)同步呼叫與非同步呼叫原始碼非同步
- React Native Android 原始碼分析之前期準備React NativeAndroid原始碼
- Flutter與Native通訊示例及原始碼分析Flutter原始碼
- Runtime原始碼 方法呼叫的過程原始碼
- ArrayList方法原始碼分析原始碼
- java 原始碼分析 —BooleanJava原始碼Boolean
- Java:HashMap原始碼分析JavaHashMap原始碼
- Java String原始碼分析Java原始碼
- 【Java】ServiceLoader原始碼分析Java原始碼
- React Native Android 原始碼分析之啟動過程React NativeAndroid原始碼
- 5.原始碼分析---SOFARPC呼叫服務原始碼RPC
- Spring原始碼分析之`BeanFactoryPostProcessor`呼叫過程Spring原始碼Bean
- java Integer 原始碼 面試必備Java原始碼面試
- BAT面試必問HashMap原始碼分析BAT面試HashMap原始碼
- Java容器原始碼學習--ArrayList原始碼分析Java原始碼
- Java-- String原始碼分析Java原始碼
- JAVA集合:ArrayList原始碼分析Java原始碼
- PowerUsageSummary.java原始碼分析Java原始碼
- 【Java集合】ArrayList原始碼分析Java原始碼
- BatteryStatsHelper.java原始碼分析BATJava原始碼
- Spring原始碼分析之AOP從解析到呼叫Spring原始碼