写点什么

java 是如何调用 native 方法?hotspot 源码分析必会技能

用户头像
诸葛小猿
关注
发布于: 2020 年 11 月 14 日
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]# lltotal 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]# lltotal 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]# lltotal 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 __cplusplusextern "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]# lltotal 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 JavaCallCJava_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.JavaCallCError: Could not find or load main class com.wuxiaolong.LB.Demo.Lesson1.JavaCallC[root@iZuf61pdvb2o7cf4mu9ccyZ java-learn]# ## 这个问题是因为最开始使用了包名,执行时报错,可以通过相关的配置解决,测试中我去掉了包名。
[root@iZuf61pdvb2o7cf4mu9ccyZ java-learn]# java JavaCallCException 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.cCclass.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.cIn 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 directoryIn 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.cCclass.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.cCclass.c: In function ‘Java_JavaCallC_cMethod’:Cclass.c:12: error: expected declaration specifiers before ‘printf’Cclass.c:13: error: expected declaration specifiers before ‘}’ tokenCclass.c:13: error: expected ‘{’ at end of input[root@iZuf61pdvb2o7cf4mu9ccyZ java-learn]### 这好像是因为Cclass.c文件方法的签名和JavaCallC.h头文件中的不一致
复制代码


关注公众号,输入“java-summary”即可获得源码。


完成,收工!



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



发布于: 2020 年 11 月 14 日阅读数: 143
用户头像

诸葛小猿

关注

我是诸葛小猿,一个彷徨中奋斗的互联网民工 2020.07.08 加入

公众号:foolish_man_xl

评论

发布
暂无评论
java是如何调用native方法?hotspot源码分析必会技能