在某些情况下,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. Flawfinder在Python2和Python3环境下对代码进行扫描方法

    1. Flawfinder Flawfinder是一款开源的关于C/C++静态扫描分析工具,其根据内部字典数据库进行静态搜索,匹配简单的缺陷与漏洞. 官网:https://dwheeler.com/f ...

  2. python3之工程中必须依赖的__init__.py

    1.  __init__.py 1.1  什么是__init__.py 在Python3工程里,当python3检测到一个目录下存在__init__.py文件时,Python3就会把它当成一个模块(m ...

  3. Java字符串的初始化与比较

    Java字符串的初始化与比较 简单的总结:直接赋值而不是使用new关键字给字符串初始化,在编译时就将String对象放进字符串常量池中:使用new关键字初始化字符串时,是在堆栈区存放变量名和内容:字符 ...

  4. iOS Swift逻辑运算符

    运算符 运算符分类 从操作数角度看:运算符包括一元.二元.三元.这里的一二三指操作数的数量,操作数指的是被操作的数值. 从运算符位置看:运算符分为前缀.中缀.后缀.例如:!b, b + c, c! 赋 ...

  5. 题解 Beautiful Pair

    题目传送门 题目大意 给出一个 \(n\) 个点的序列 \(a_{1,2,...,n}\) ,问有多少对点对 \((i,j)\) 满足 \(a_i\times a_j\le a_k(i\le k\le ...

  6. postman如何解决下个接口依赖上一个接口数据

    解决思路:将上一个接口返回的数据保存起来,然后提供下个接口使用,postman中通过设置全局变量来保存数据 步骤1:在Tests中添加如下图js代码. var jsondata = JSON.pars ...

  7. 一次简单的SQL注入绕WAF

    本人也是小白一枚,大佬请绕过,这个其实是六月份的时候做的,那时候想多点实战经验,就直接用谷歌搜索找了一些网站,这个是其中一个 1.目标网站 2.发现有WAF防护 3.判断存在注入 4.猜测了一下闭合为 ...

  8. 【数据结构】<栈的应用>回文判断

    通过栈与队列相关内容的学习,我们知道,栈是"先进后出"的线性表,而队列是"先进先出"的线性表.可以通过构造栈与队列来实现在这一算法.将要判断的字符序列依次压栈和 ...

  9. HCNP Routing&Switching之BGP路由宣告

    前文我们了解了BGP报文结构.类型以及邻居状态相关话题,回顾请参考https://www.cnblogs.com/qiuhom-1874/p/15422924.html:今天我们来聊一聊BGP路由宣告 ...

  10. Java中的函数式编程(六)流Stream基础

    写在前面 如果说函数式接口和lambda表达式是Java中函数式编程的基石,那么stream就是在基石上的最富丽堂皇的大厦. 只有熟悉了stream,你才能说熟悉了Java 的函数式编程. 本文主要介 ...