在学习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”即可获得源码。
完成,收工!
【传播知识,共享价值】,感谢小伙伴们的关注和支持,我是【诸葛小猿】,一个彷徨中奋斗的互联网民工。