- 提高程序性能;
- 实现某些
纯Java代码不可能实现
的功能; - 使用其他语言的类库;
-
与硬件、操作系统进行交互。
What is JNI ?JNI是
How to write application with JNI ? step1.定义native方法Java Native Interface
的缩写,通过使用native
关键字书写程序,允许Java与其他语言
进行交互。public class Main { public static native String helloJni();
}
#### step2.生成头文件
我们使用命令生成c语言使用的`头文件`。
```shell
javac -h . Main.java
# 两个命令都可以,但是从JDK10开始javah被废弃
# 因此推荐使用上面的命令
javah Main
下面是生成头文件Main.h
的具体内容:
/* DO NOT EDIT THIS FILE - it is machine generated */
#include <jni.h>
/* Header for class Main */
#ifndef _Included_Main
#define _Included_Main
#ifdef __cplusplus
extern "C" {
#endif
/*
* Class: Main
* Method: helloJni
* Signature: ()Ljava/lang/String;
*/
JNIEXPORT jstring JNICALL Java_Main_helloJni
(JNIEnv *, jclass);
#ifdef __cplusplus
}
#endif
#endif
step3.编写native的实现MainImpl.c
#include <jni.h>
#include <jni_md.h>
#include <jvmti.h>
#include "Main.h"
JNIEXPORT jstring JNICALL Java_Main_helloJni
(JNIEnv *env, jclass klass) {
return env->NewStringUTF("Hello JNI");
}
step4.生成动态链接库
我的JAVA_HOME
为/Library/Java/JavaVirtualMachines/zulu-8.jdk/Contents/Home
,对应生成动态链接库的命令为:
g++ -I /Library/Java/JavaVirtualMachines/zulu-8.jdk/Contents/Home/include
-I /Library/Java/JavaVirtualMachines/zulu-8.jdk/Contents/Home/include/darwin
-I /Users/admin/Downloads/study/jni/src/main/native
MainImpl.c -m64 -fPIC -shared -o jni.dylib
特别注意:
-I
要包含JAVA_HOMEinclude
文件夹下的全部文件夹,不同平台的include
子文件夹不一样;- 如果是
32位
的操作系统,需要把命令中的-m64
改为-m32
; - 不同平台生成的
动态链接库
后缀不同,比如linux是.so
,mac是.dylib
、.so
,windows是.dll
; step5.加载动态链接库
step6.调用native方法java.lang.System#load
直接像调用一个java方法一样调用它就好了,下面附上完整代码:
import java.net.URL;
public class Main {
static {
final URL url = Main.class.getResource("jni.dylib");
System.load(url.getPath());
}
public static native String helloJni();
public static void main(String[] args) {
System.out.println(helloJni());
}
}
诚如您所见,编写一个使用了JNI的Java程序并不难!
## Generic JNI
### JNI shortcoming
- 使用Java与`动态链接库`交互,通常会`丧失`JVM平台的可移植性,这意味着要我们`自己兼容`不同的平台。
### Compatible JNI
在vmtool正式贡献之前,我尝试了几种方案来生成`动态链接库`:
1. `Runtime.getRuntime().exec("......")`动态生成,失败,由于安全问题,此API在生产环境直接被禁用了;
2. 交叉编译,失败,根本找不到Java生态可调用的api;
3. 安装`vmware`并安装不同平台的`虚拟机`,然后在虚拟机上打不同的`动态链接库`,失败,真实原因由于个人水平有限不得而知,猜测是打包调用时最终会调用到底层的操作系统,而操作系统之间不互通;
4. `native-maven-plugin`,成功,底层仍是使用`Runtime`API,只是因为打包的机器没有禁用`Runtime`相关API,所以能成功;
## Better JNI
### JDK的坑
使用`native-maven-plugin`时需要配置JDK中包含头文件的目录名(对于`Oracle JDK`其实就是`include`),[但是对于其他`JDK`可能就不是`include`目录了](http://github.com/alibaba/arthas/pull/1698#issuecomment-785887313) 。
怎么解决这个问题呢?
作者的做法是把不同平台的JDK都下一遍,再对它们的`include`文件夹做整合,最终才呈现给大家`arthas-vmtool/src/main/native/head`。
### 警惕内存泄露
在vmtool最初的 [PR](https://github.com/alibaba/arthas/pull/1698) 里,调用`GetObjectsWithTags`、`GetLoadedClasses`后没有`释放内存`的代码,这也就导致了`必定`发生的`内存泄漏`,提完 [PR](https://github.com/alibaba/arthas/pull/1698) 后,我没有注意到`部分代码`存在`本地方法栈内存泄漏`(不了解的同学建议阅读周志明的《深入理解Java虚拟机》),幸亏 [kylixs](https://github.com/kylixs) 发现并立刻通知,才让内存泄漏问题在vmtool正式发布之前被解决,在此鸣谢。
敏锐的读者可能已经察觉到了,`不是`调用所有的JVMTI方法都要编写释放内存的逻辑,那么调用JVMTI的哪些方法要编写呢?请参考[JVMTI手册](https://docs.oracle.com/javase/8/docs/platform/jvmti/jvmti.html) 。
### 干掉不必要的回调
最开始`getInstances0`的返回结果是`List<T>`而不是`T[]`:
```java
static native <T> List<T> getInstances0(Class<T> klass, int limit);
这意味着要在c
的代码中回调java的java.util.ArrayList#add
,当这种回调用达到一个量级后,能明显看到调用所耗费的时间。
作者记得之前跑一个benchmark花了5min
,干掉不必要的回调、改成返回T[]
后,再跑benchmark发现只耗费1min
了,由此可见提升是多么地巨大。
前面铺垫了那么多,终于进入源码分析的正题了,我们以arthas.VmTool#getInstances0
为例分析。
初始化JVMTI
(后续遍历堆
、从堆中获取类实例
和释放内存
都依赖于JVMTI
,读者可以理解为JNI
包含了JVMTI
):
static jvmtiEnv *jvmti;
//这里的extern "C"是为了向下兼容C
extern "C"
int init_agent(JavaVM *vm, void *reserved) {
//获取JVMTI
jint rc = vm->GetEnv((void **)&jvmti, JVMTI_VERSION_1_2);
if (rc != JNI_OK) {
fprintf(stderr, "ERROR: arthas vmtool Unable to create jvmtiEnv, GetEnv failed, error=%d\n", rc);
return -1;
}
//配置JVMTI
jvmtiCapabilities capabilities = {0};
capabilities.can_tag_objects = 1;
jvmtiError error = jvmti->AddCapabilities(&capabilities);
if (error) {
fprintf(stderr, "ERROR: arthas vmtool JVMTI AddCapabilities failed!%u\n", error);
return JNI_FALSE;
}
return JNI_OK;
}
//通过premain方式启动JavaAgent会回调此方法
extern "C" JNIEXPORT jint JNICALL
Agent_OnLoad(JavaVM *vm, char *options, void *reserved) {
return init_agent(vm, reserved);
}
//通过attach方式启动JavaAgent会回调此方法
extern "C" JNIEXPORT jint JNICALL
Agent_OnAttach(JavaVM* vm, char* options, void* reserved) {
return init_agent(vm, reserved);
}
//通过java.lang.System.load或者java.lang.System.loadLibrary动态加载动态链接库会回调此方法
extern "C" JNIEXPORT jint JNICALL
JNI_OnLoad(JavaVM* vm, void* reserved) {
init_agent(vm, reserved);
return JNI_VERSION_1_6;
}
step2.遍历堆
我们需要获取某个类的实例怎么办?遍历堆吧。
static LimitCounter limitCounter = {0, 0};
extern "C"
JNIEXPORT jobjectArray JNICALL
Java_arthas_VmTool_getInstances0(JNIEnv *env, jclass thisClass, jclass klass, jint limit) {
//这里获取一个唯一标记
jlong tag = getTag();
//这里初始化计数器
limitCounter.init(limit);
//......
}
出于性能方面的考虑,VmTool#getInstances0
默认只会获取JVM上某个类的10
个实例,也就是说我们遍历堆,一旦发现已经有10个实例就没必要继续遍历了,那么怎么记录已遍历的实例数量
呢?借助于arthas
自定义的LimitCounter
。
struct LimitCounter {
//已遍历过的实例数
jint currentCounter;
//需要的实例数,<0则表示需要堆中的所有实例
jint limitValue;
void init(jint limit) {
currentCounter = 0;
limitValue = limit;
}
void countDown() {
currentCounter++;
}
bool allow() {
if (limitValue < 0) {
return true;
}
return limitValue > currentCounter;
}
};
真正去遍历堆:
extern "C"
jvmtiIterationControl JNICALL
HeapObjectCallback(jlong class_tag, jlong size, jlong *tag_ptr, void *user_data) {
//对符合要求的对象打上标记
jlong *data = static_cast<jlong *>(user_data);
*tag_ptr = *data;
//已遍历的count数增加
limitCounter.countDown();
if (limitCounter.allow()) {
//没到限制继续遍历
return JVMTI_ITERATION_CONTINUE;
} else {
//超过限制就不遍历了
return JVMTI_ITERATION_ABORT;
}
}
extern "C"
JNIEXPORT jobjectArray JNICALL
Java_arthas_VmTool_getInstances0(JNIEnv *env, jclass thisClass, jclass klass, jint limit) {
//......
//遍历堆
jvmtiError error = jvmti->IterateOverInstancesOfClass(klass, JVMTI_HEAP_OBJECT_EITHER,
HeapObjectCallback, &tag);
if (error) {
printf("ERROR: JVMTI IterateOverInstancesOfClass failed!%u\n", error);
return NULL;
}
//......
}
step3.从堆中获取已标记的实例
extern "C"
JNIEXPORT jobjectArray JNICALL
Java_arthas_VmTool_getInstances0(JNIEnv *env, jclass thisClass, jclass klass, jint limit) {
//......
jint count = 0;
jobject *instances;
error = jvmti->GetObjectsWithTags(1, &tag, &count, &instances, NULL);
if (error) {
printf("ERROR: JVMTI GetObjectsWithTags failed!%u\n", error);
return NULL;
}
//......
}
step4.把获取到的实例添加到数组
extern "C"
JNIEXPORT jobjectArray JNICALL
Java_arthas_VmTool_getInstances0(JNIEnv *env, jclass thisClass, jclass klass, jint limit) {
//......
//创建一个数组
jobjectArray array = env->NewObjectArray(count, klass, NULL);
for (int i = 0; i < count; i++) {
//添加元素到数组
env->SetObjectArrayElement(array, i, instances[i]);
}
//......
}
step5.释放内存并返回结果
extern "C"
JNIEXPORT jobjectArray JNICALL
Java_arthas_VmTool_getInstances0(JNIEnv *env, jclass thisClass, jclass klass, jint limit) {
//......
//释放内存
jvmti->Deallocate(reinterpret_cast<unsigned char *>(instances));
//返回结果
return array;
}
Regret
唯一的、最大的遗憾就是vmtool模块
不能单独使用
,如果可以单独使用的话,vmtool在获取类实例
上提供了远比Spring
强大的功能(Spring只能获取由BeanFactory
实例化的instance
,而vmtool可以获取JVM
级别的instance
)。