在某些情况下,Java语言需要通过调用C/C++函数来实现某些功能,因为Java有时候对这些功能显的无能为力,如想使用X86_64 的 SIMD 指令提升一下业务方法中关键代码的性能,又或者想要获取某个体系架构或者操作系统特有功能的支持。为了能在Java 代码中调用 C/C++函数,JVM提供了Java Native Interface(JNI)机制。 在Java中,使用native关键字标注的、没有方法体的方法就是native方法。当在 Java 代码中调用这些 native 方法时,Java 虚拟机将通过JNI调用到对应的 C/C++ 函数。那么普通的Java方法和native方法有什么区别呢?

native方法与java普通方法的区别:

(1)普通Java方法在解释执行情况下,调用dispatch_next()函数执行每一条字节码指令并达到解释执行的效果,而本地C/C++函数会通过C/C++编译器编译为机器指令执行,所以Java方法可能会采用解释执行,而C/C++函数会编译执行;

(2)普通Java方法(包括普通Java同步方法)的入口例程是由HotSpot VM的generate_normal_entry()函数生成的,而native方法(包括native同步方法)的入口例程是由generate_native_entry()函数生成的。在对同步方法进行处理时,generate_normal_entry()函数中调用lock_method()函数生成例程,这个例程会对Java方法加锁而没有对应的释放锁逻辑,因为dispatch_next()函数执行字节码指令时,一些字节码如return、athrow在移除栈帧的时候会有释放锁的操作,所以无须生成释放锁的逻辑,但是generate_native_entry()函数生成的例程没有执行字节码指令,它必须在执行完native方法之后检查是否需要执行释放锁操作。generate_native_entry()函数生成的例程到目前为止还没有介绍,不过后面我们马上会介绍。

我之前在开发某个性能故障排查工具时,因为这个工具需要支持不同的操作系统,所以我选择使用Java语言开发,但是在开发过程中需要根据进程pid来获取应用程序的执行目录,而Java的核心库又无法提供出这样的功能,所以我只能借助JNI机制来开发。通过这样的开发方式虽然能满足一定的需求,但是不要忘记,这会牺牲可移植性,我需要在linux、Mac和Windows平台上生成各自的.so、.jnilib和.dll动态链接库,非常的麻烦。另外在使用JNI机制开发时,还有一些缺点,如下:

  • 从 Java 环境到 native code 的上下文切换耗时、低效;
  • JNI 编程,如果操作不当,可能引起 Java 虚拟机的崩溃;
  • JNI 编程,如果操作不当,可能引起内存泄漏;

下面举一个JNI实例,如下:

public class TestJNI {
static {
// 程序在加载时,自动加载libdiaoyong.so库
System.loadLibrary("diaoyong");
} // 声明原生函数。注意要添加native关键字
public native void set(int value); public native int get(); public static void main(String[] args) {
TestJNI test = new TestJNI();
test.set(1);
System.out.println(test.get());
}
}

调用JNI的时候,通常使用System.loadLibrary()方法加载JNI library,同样也可以使用System.load()方法加载JNI library,两者的区别是一个只需要设置库的名字,比如如果动态链接库的名称为libA.so,则只要输入A就可以了,而libA.so的位置可以通过设置java.library.path或者sun.boot.library.path指定,而System.load()方法需要输入完整路经的文件名。

下面编写native方法对应的C/C++函数的本地实现,如下:

// 命令生成java.class文件。假设TestJNI在包com/test下,则也是在com/test下使用这个命令生成java.class文件。
javac TestJNI.java // 命令生成TestJNI.h文件。假设TestJNI在包com/test下,则要切换到com/test的上一级后
// 使用javah -jni com.test.TestJNI这个命令生成类似于com_test_TestJNI.h文件。
javah -jni TestJNI

生成的TestJNI.h文件的内容如下:

/* DO NOT EDIT THIS FILE - it is machine generated */
#include <jni.h>
/* Header for class TestJNI */ #ifndef _Included_TestJNI
#define _Included_TestJNI
#ifdef __cplusplus
extern "C" {
#endif
/*
* Class: TestJNI
* Method: set
* Signature: (I)V
*/
JNIEXPORT void JNICALL Java_TestJNI_set(JNIEnv *, jobject, jint); /*
* Class: TestJNI
* Method: get
* Signature: ()I
*/
JNIEXPORT jint JNICALL Java_TestJNI_get(JNIEnv *, jobject); #ifdef __cplusplus
}
#endif
#endif

JNIEXPORT和JNICALL都是JNI的关键字,表示此函数是要被JNI调用的。jint是以JNI为中介使Java的int类型与本地的int类型沟通的一种类型。函数的名称是Java_Java程序的package路径_函数名组成的。

现在我们规范一下术语,如下:

除了native方法,本地函数外,还有JNI函数,这是HotSpot VM为本地函数提供的,用来访问HotSpot VM内部服务的函数。

下表详细介绍了Java中与C/C++中类型的对应关系。

Java JNI中的别名 C/C++中的类型 字节数
boolean jboolean unsigned char 1
byte jbyte signed char 1
char jchar unsigned short 2
short jshort short 2
int jint/jsize long 4
long jlong __int64 8
float jfloat float 4
double jdouble double 8

jobject是个JNI句柄,或称为native句柄、本地句柄。在开发native时,经常会用到这个句柄,如果native方法是个实例(非静态)方法,生成的本地函数第2个参数类型就是jobject,用于表示该native方法所对应的Java对象的JNI句柄。

// C++使用的_jobject的定义
class _jobject {};
typedef _jobject *jobject; // C使用的_jobject的定义
struct _jobject;
typedef struct _jobject *jobject;

jobject是_jobject类型的指针,我们在实际过程中可以这样使用:

jobject handle = ...
oop* ptr = (oop*)handle;

JNI句柄可以直接转换为一个oop指针。jobject是指针类型,oop*明显也是指针类型,不过由于oop本身就是指针类型,所以handle可以说是指针的指针。 

对于数组类型的对应关系如下表所示。

Java C/C++
boolean[ ] JbooleanArray
byte[ ] JbyteArray
char[ ] JcharArray
short[ ] JshortArray
int[ ] JintArray
long[ ] JlongArray
float[ ] JfloatArray
double[ ] JdoubleArray

对于本地函数来说,函数的名称默认一般为“Java_Java程序的package路径_函数名”组成的。 

本地函数的第一个参数JNIEnv接口指针,指向一个函数表,函数表中的每一个入口指向一个JNI函数。本地函数经常通过这些函数来访问HotSpot中的数据结构,如堆中的oop等。下图演示了JNIEnv这个指针:

本地函数的第二个参数根据native方法是一个静态方法还是实例方法而有所不同。本地方法是一个静态方法时,第二个参数代表本地方法所在的类;本地方法是一个实例方法时,第二个参数代表本地方法所在的对象。如上例子的Java_TestJNI_get()函数与Java_TestJNI_set()函数是native实例方法的本地实现,因此jobject参数指向方法所在的对象。 

继续编写对应的c语言的实现,如下:

#include <stdio.h> 

#include "TestJNI.h" 

int i=0; 

JNIEXPORT void JNICALL Java_TestJNI_set(JNIEnv * env, jobject obj, jint j) {
i=j*888;
} JNIEXPORT jint JNICALL Java_TestJNI_get(JNIEnv * env, jobject obj){
printf("ok!You have successfully passed the Java call c\n");
return i;
}

对于obj来说,如果native方法不是static的话,这个obj就代表这个native方法的类实例。如果native方法是static的话,这个obj就代表这个native方法的类的Class对象(static方法不需要类实例,所以就代表这个类的Class对象)

使用如下命令生成TestJNI.o文件。

gcc -Wall -fPIC -c TestJNI.c
-I ./ \
-I /home/mazhi/workspace/jdk1.8.0_192/include/linux/ \
-I /home/mazhi/workspace/jdk1.8.0_192/include/

命令中的参数解析如下:

-Wall:打开警告开关。

-fPIC:表示编译为位置独立的代码,不用此选项的话编译后的代码是位置相关的所以动态载入时是通过代码拷贝的方式来满足不同进程的需要,而不能达到真正代码段共享的目的。

gcc -Wall -rdynamic -shared -o libdiaoyong.so TestJNI.o

命令中的参数解析如下:

动态链接库的名字必须是 lib*.so,因为编译器查找动态连接库时有隐含的命名规则,即在给出的名字前面加上lib,后面加上.so来确定库的名称。这里是libdiaoyong.so对应于Java程序里的diaoyong。

选项 -rdynamic 用来通知链接器将所有符号添加到动态符号表中。

-shared指编译后会链接成共享对象。

编译~/.bashrc文件,添加环境变量的配置export LD_LIBRARY_PATH=./ 使用source ~/.bashrc命令使配置生效。之前在TestJNI类中的如下调用:

System.loadLibrary("diaoyong");

意思就是生成的动态库文件名为libdiaoyong.so(这是linux环境)(如果是window环境,则为diaoyong.dll)。这里可能有人就会问,这个libdiaoyong.so文件应该放在哪里呢?

这个需要放到linux系统下的JNI环境中,也就是说必须声明一个环境变量,对应一个文件夹,然后这个文件就放在这个文件夹下面就可以找到了。

最后通过java TestJNI命令对运行Java程序后,可以看到正确的输出结果。

公众号 深入剖析Java虚拟机HotSpot 已经更新虚拟机源代码剖析相关文章到60+,欢迎关注,如果有任何问题,可加作者微信mazhimazh,拉你入虚拟机群交流

  

第39篇-Java通过JNI调用C/C++函数的更多相关文章

  1. JAVA使用JNI调用C++动态链接库

    JAVA使用JNI调用C++动态链接库 使用JNI连接DLL动态链接库,并调用其中的函数 首先 C++中写好相关函数,文件名为test.cpp,使用g++编译为DLL文件,指令如下: g++ -sha ...

  2. Java通过JNI调用dll详细过程(转)

    源:Java通过JNI调用dll详细过程 最近项目有这样一个需求,在已有的CS软件中添加一个链接,将当前登录用户的用户名加密后放在url地址中,在BS的login方法里通过解密判断,如果为合法用户则无 ...

  3. ubuntu下Java通过JNI调用C

    下面看一个实例,如下: public class TestJNI { static { System.loadLibrary("diaoyong"); // 程序在加载时,自动加载 ...

  4. JAVA的JNI调用

    由于JNI调用C和调用C++差不多,而且C++中可以混合写C代码,所以这里主要是写关于JNI调用C++的部分. 一般步骤: 先是写普通的Java类,其中包括本地方法调用.  然后编译这个Java类,调 ...

  5. java jni 调用c语言函数

    今日在hibernate源代码中遇到了native关键词,甚是陌生,就查了点资料,对native是什么东西有了那么一点了解,并做一小记. native关键字说明其修饰的方法是一个原生态方法,方法对应的 ...

  6. Java通过JNI调用C

    Java调用C有多种方式,本文介绍笔者最近的学习过程,避免今后再犯类似的错误. 首先,Java肯定是调用C的动态链接库,即通过编译器编译后的dll/so文件. 下面介绍gcc编译dll的方法. 一般情 ...

  7. Cocos2d-x java 通过jni调用c++的方法

    前面博客说到,cocos2d-x c++界面层运行在一个GLThread线程里面,严格的说是运行在Cocos2dxGLSurfaceView(继承自GLSurfaceView) 里面.opengl的渲 ...

  8. 关于Java通过JNI调用C 动态链接库(DLL)

    JNI介绍 用JNI实现Java和C语言的数据传递 JNI原理分析和详细步骤截图说明 jni的JNIEnv指针和jobject指针 JNI实现回调| JNI调用JAVA函数|参数和返回值的格式 Jni ...

  9. Java通过jni调用动态链接库

    (1)JNI简介 JNI是Java Native Interface的缩写,它提供了若干的API实现了Java和其他语言的通信(主要是C&C++).从Java1.1开始,JNI标准成为java ...

随机推荐

  1. phpstorm 配置Psr4 风格代码

    http://www.cnblogs.com/xp796/p/6441700.html

  2. 直接取PHP二维数组里面的值

    具体是这样的,如下一个二维数组,是从库中读取出来的. $user = array( 0 => array( 'id'    => 1, 'name'  => '张三', 'email ...

  3. Python-对Pcap文件进行处理,获取指定TCP流

    通过对TCP/IP协议的学习,本人写了一个可以实现对PCAP文件中的IPV4下的TCP流提取,以及提取指定的TCP流,鉴于为了学习,没有采用第三方包解析pcap,而是对bytes流进行解析,其核心思想 ...

  4. go 成长路上的坑(2)

    请看代码 代码一 package main import "fmt" func main() { var a Integer = 1 var b Integer = 2 var i ...

  5. 牛客挑战赛48C-铬合金之声【Prufer序列】

    正题 题目链接:https://ac.nowcoder.com/acm/contest/11161/C 题目大意 \(n\)个点加\(m\)条边使得不存在环,每种方案的权值是所有联通块的大小乘积. 求 ...

  6. 一篇文章告诉你Python接口自动化测试中读取Text,Excel,Yaml文件的方法

    前言 不管是做Ui自动化和接口自动,代码和数据要分离,会用到Text,Excel,Yaml.今天讲讲如何读取文件数据 Python也可以读取ini文件,传送门 记住一点:测试的数据是不能写死在代码里面 ...

  7. Dapr逐渐被点燃

    Dapr被点燃 Dapr的热度个人认为才刚刚热起来,9月份我写了Dapr + .NET Core实战一共10篇,从基础概念到简单的实战,但是有很多人感兴趣,具体表现在我个人维护的QQ群,人数从80人左 ...

  8. js高阶

    1. 面向对象编程介绍 1.1 两大编程思想 --- 面向过程 --- 面向对象 1.2 面向过程编程 POP 面向过程就是分析出解决问题所需要的步骤,然后用函数把这些步骤一步一步实现,使用的时候在一 ...

  9. Vulnhub实战-doubletrouble靶机👻

    Vulnhub实战-doubletrouble靶机 靶机下载地址:https://www.vulnhub.com/entry/doubletrouble-1,743/ 下载页面的ova格式文件导入vm ...

  10. Jmeter使用问题记录

    Jmeter下载安装,设置中文,返回值乱码处理,下载接口测试 下载地址 解压后,在Jmeter的bin文件夹下启动 修改默认启动为中文简体:打开bin目录下的jmeter.properties文件,在 ...