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”即可获得源码。

完成,收工!

传播知识,共享价值】,感谢小伙伴们的关注和支持,我是【诸葛小猿】,一个彷徨中奋斗的互联网民工。

猜你喜欢

转载自blog.csdn.net/wuxiaolongah/article/details/109685475