java是如何呼叫native方法?hotspot原始碼分析必會技能

諸葛小猿發表於2020-11-14

在學習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”即可獲得原始碼。

完成,收工!

傳播知識,共享價值】,感謝小夥伴們的關注和支援,我是【諸葛小猿】,一個彷徨中奮鬥的網際網路民工。

相關文章