第39篇-Java通过JNI调用C/C++函数

在某些情况下,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路径_函数名组成的。

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

第39篇-Java通过JNI调用C/C++函数 

除了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这个指针:

第39篇-Java通过JNI调用C/C++函数 

本地函数的第二个参数根据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++函数

  

 

上一篇:39.第六章 Shell脚本编程高级进阶(四)


下一篇:【LeetCode】39. 组合总和