开发者学堂课程干货总结——Java 虚拟机原理(一)

各位同学,开发者学堂Java 图谱中Java 高级工程师篇的课程“Java 虚拟机原理”的课程给开始更新了,第一课时“JNI in Java​”的干货总结来啦!一起学习新课程吧!

课程链接以及图谱地址小编已经为大家指路了,搭配学习效果更佳????

课程名称:JNI in Java

课程地址:https://developer.aliyun.com/learning/course/56/detail/1190

图谱名称:Alibaba Java 技术图谱

图谱地址:https://developer.aliyun.com/graph/java


JNI in Java


一、什么是JNI 

(一)什么是JNI (Java Native Interface) 

JNI全称是Java Native Interface,顾名思义是Java和Native间的通信桥梁,如下图所示,图的上方是Java世界,下面是Native世界,中间是JNI通信,左边箭头从上往下是Java调用Native的方法,右边是Native调用Java,彼此可以互通。 

开发者学堂课程干货总结——Java 虚拟机原理(一) 

这种方式带来的好处Java调用Native,可以去调用非Java实现的库,扩充Java的使用场景比如调用Tensorflow反之Native调用Java,可以在别的语言里面调用Java,比如java launcher可以命令启动Java程序 

(二)为什么要学习JNI 

掌握Java和Native之间的互相调用,大大丰富java的使用场景了解原理,对于学习JVM/故障定位更加得心应手 

经典例子,如下图所示,在主函数里面用Selector.open创建一个selectselect方法,这是Java里面通过NIO取允许网络的方法。 

开发者学堂课程干货总结——Java 虚拟机原理(一) 

public static void main(String[] args) throws Exception { 

        java.nio.channels.Selector.open().select(); 

    } 

这个方法会阻塞其当前线程通过java.lang呈现状态是RUNNABLE看到RUNNABLE总觉得消耗CPU、NIO的BUG, 其实是一个经典谬误,实际上线程是禁止的 

 

二、JNI实践和思考 

实战: 从native调用Java 

首先#include <jni.h> 这个头文件定义了各种Java和Native交互的数据结构以及定义在主函数里面,首先声明一个JVM的指针,然后一个JNIEnv *env的指针,JVM表示的Java虚拟实,我通过实例消耗资源进行各种操作。 

env其实对应的是一个线程,然后创建JavaVMInitArgs结构体结构体里面要填充Java参数,JavaVMOption表示因为这里不需要参数,场景比较简单,所以用options[0]把 options传入 vm_args.options结构体,最后调用JNI_CreateJavaVM创建 Java虚拟器,如果返回的是JNI_OK,说明这次调用成功 

有了JNI指针表示实例以后,就可以用标准方法使用JNI在这里调用一个Java方法,比如Java数据结构先通过EMCFindClass找到SelectorProvider类,中间有个printf变量叫lock,先通过 GetStaticField获取 field再通过GetStaticObjectField从 cls对象上获取fid就是 lock对象,然后把它打印出来,最后jvm->DestroyJavaVM。详情操作如下图所示: 

开发者学堂课程干货总结——Java 虚拟机原理(一) 

还有一个比较经典的例子Java Launcher java –jar spring-application执行程序的时候,在后台默默的创建了一个jvmJava参数作为 arguments传进去,调用Java入口方法通过JNI实现 

开发者学堂课程干货总结——Java 虚拟机原理(一) 

平时说,开发jvm其实就是开发jvm的动态库, libjvm.so基本上本身是作为os提供出去,好处是非常灵活,可以作为独立应用使用,也可以在别的像cer这样的语言调用使Java调用NativeNative调用Java灵活。 

 

JNI实战Java调用C 

Java调用C使用JNI最常见的方式首先定一个类叫HelloJNI里面有System.loadLibrary("hello"); 系统会自动去找到library libhello.so这个类里面定义方法叫sayHello,加了C以后调用它,但这是调不通的,因为并没有提供真正的Native实现实现要通过一个头文件去告诉这个方法的签名,这里实现Java文件,然后通过jni.h生成头文件,这个是自动生成的。 

签名是 Java,然后是Java_HelloJNI_sayHello(JNIEnv *, jobject)规范,类名加上方法名,参数第一个是环境第二个是jobject,无参数,但是 Java的方法默认是有一个this指针作为第一个参数,最后编写它,实现HelloJNI.c根据这个声明定义实现,然后里面只是printf一下,把 HelloJNI.c定义成libhelloHello.so这个程序就可以运行起来了。详情如下图所示: 

开发者学堂课程干货总结——Java 虚拟机原理(一) 

在Java应用里面,可以调通过JNI调用各种库,调用到native以后,因为任何语言跟native都互相交互,大大丰富了Java使用场景。 

开发者学堂课程干货总结——Java 虚拟机原理(一) 

 

思考Java和Native的数据是怎么传递的 

在执行Java方法时,用的是java heap,假设暂时向下增长,需要调用 c函数的时候,它需要去压站,把 object压站JNIEnv压站cstack压站,进入seat stack然后 object本质上是指向handle的指针,handle指向战上真正的OOP,使用二级指针结构,稍微有点复杂。详情如下图所示: 

开发者学堂课程干货总结——Java 虚拟机原理(一) 

 

思考: 回到问题,为什么select()的线程状态是RUNNABLE 

JNI只是提供一种机制,让Java程序可以进入Native状态,Native状态基本上没有办法管理。这段Native代码在做一种非常复杂的数学运算,肯定是RUNNABLE状态,也可以调用系统形象去阻塞,但这个阻塞基本上不知情,所以会一直显示为RUNNABLE,除非通过JNI的特殊接口改变现实状态,到其他状态才会显示为其他状态,所以这里显示为RUNNABLE正常不用担心RUNNABLE状态消耗很多CPU问题。 

开发者学堂课程干货总结——Java 虚拟机原理(一) 

 

三、JNI与safepoint 

首先有这样两个问题: 

1JNI是否会影响GC进行 

2、GC时JNI修改Java Heap怎么保证一致 

看到第二个问题的时候,已经回答第一个问题,假如GC是不能运行JNI,那也就没有一致性问题,所以在GC可以执行JNI 

 

(一)JNI与Safepoint的协作 

首先要知道Java的信任状态,Java最主要信任状态是Thread in Java状态,这状态里面在执行一个解释器或者已经编译的方法,纯Java执行这时候如果发生Safepoint会通过Interpreter机制把这个线程直接挂起,暂停下来,然后去Safepoint里面进行GC的各种操作。 

在Java里面,调用JNI进入Native会切换到Thread in native状态,这里执行Native函数,在执行的时候跟GC可以并行执行,因为理论上要么执行,要么通过JNIJNI交互,所有的跟JNI相关的数据结构都可以被管理。然后Native还可以去切换到JVM状态,这是非常关键的状态,这个状态不能发生GC不用关心 

JNI与Safepoint交互,假如JNI执行时发生Safepoint能并行JNI执行的时候返回Java,这时候会被阻塞需要检查状态,卡在Safepoint状态,直到Safepoint结束,继续回到Java。 

开发者学堂课程干货总结——Java 虚拟机原理(一) 

(二)JNI与GC 

透过几个JNI管中窥豹了解这个机制 

void * GetPrimitiveArrayCritical(JNIEnv *env, jarray array, jboolean *isCopy);  

void ReleasePrimitiveArrayCritical(JNIEnv *env, jarray array, void *carrayjint mode); 

这个函数叫做GetPrimitiveArrayCriticalCritical作用是把一段内存返回给用户,用户可以直接编辑里面的数据这时如果发生GC被移动编辑肯定会导致 heap乱掉,Critical这段时间里锁住heap没法发生GC。假如 critical状态发生期间,基本上不会影响GC会等待,直ReleasePrimitiveArrayCritical发出这是比较巧妙的互相协作。 

下图所示的二级指针模型还是前面Java调到Native,参数通过jobjecthandle保存使用jobject指向handlehandle指向oop 

开发者学堂课程干货总结——Java 虚拟机原理(一) 

java heap时候假如OOP对象被移动handle,同时会更新 handle里面的地址所以只要C程序都是通过JNI访问对象,每次对象被移动它都可以被感知,不会出现数据布局之后突然情况 

“GC: handle_are->oops_do(f)” 

指有区域专门存放handle,里面所有handle在GC里,都会进行一次指针修正,保证数据一致性。 

四、JNI与Intrinsic 

(一)高级主题: intrinsic 

如下图所示,非常常见JNA“currentThread为例子,说明Intrinsic机制。Intrinsic在看到currentThread的时候不会去JNI,而是通过形成更高效的版本。 

这里inline_native_currentThread的时候最终会调用generate_curent_thread工具然后看里面的实现核心部分,创建ThreadLocalNode(),代表当前JavaThread结构的指针,再通过JavaThread结构里的threadObj_offset()拿到它,通常是一个偏移量,拿到Object以后作为返回值返回这里是一段AI,真正生成代码被翻译成非常简约的几条指令,直接返回。所以currentThread变得非常高效,这就是Intrinsic机制,主要为性能而生 

开发者学堂课程干货总结——Java 虚拟机原理(一) 

开发者学堂课程干货总结——Java 虚拟机原理(一) 

 

(二)Intrinsic性能分析 

对比一下IntrinsicIntrinsic性能,如下图所示,是jmh写的Benchmark,可以规避掉一些具体的预热不够导致性能测试不准问题,用它进行测试,也是官方推荐的版本 

Intrinsic版本,下面测试叫“jni”,主要区别就是Intrinsic后面接了一个叫isAlive的调用。isAlive本身状态调用看起来非常轻量,但因为他没有做Intrinsic,所以最终会走JNI 

开发者学堂课程干货总结——Java 虚拟机原理(一) 

 

下图所示,对比普通Intrinsic加上JNIIntrinsic性能,普通 Intrinsic的性能大概是3亿次每秒加上JNI的Intrinsic版本的性能是2000万次每秒,差了十几倍,差距很大 

开发者学堂课程干货总结——Java 虚拟机原理(一) 

 

进一步性能问题,最重要的是performingperforming手段performpublic第二段JNI版本,前面两个热点方法都是ThreadStateTransition状态转换。前面说到,假如JNI回到 Java时候做GC肯定要停下来,所以这有个内存同步比较好资源,要等的时间比较长,所以这两个函数是最热的。 

开发者学堂课程干货总结——Java 虚拟机原理(一) 

下面是JVM_IsThreadAlive实现。后面是HandleMark::pop_and_restore在调JNI时需要把oop包装handle,JNI退出时需要消费handle restore有开销。再后面java_lang_Thread::is_alive占比4.77% 非常小。 

由此可以看出Intrinsic提供性能非常好的机制,直接调用JNI性能可能差一点,但也可以接受 

 

(三)案例分析: RocketMQ Intrinsic导致应用卡顿 

RocketMQ 是阿里巴巴开源的MQ产品,使用非常广泛里面有个函数叫warmMappedFile指的是RocketMQ是通过warmMapped机制内存映射磁盘去做IO,在申请完一块磁盘映射的内存以后,会去做预热。 

这里有for循环for (int i = 0, j = 0; i < this.fileSize”,每隔一个PACG_SIZEbyteBuffer.put(i, (byte) 0);,这样的话操作系统就会发生缺,把内存真正分配出来,而不只是EMV数据结构。分配出来以后,等到程序真正使用这块内存的时候,就是纯内存IO,不太会触发这种缺页了,可以变得更快,目的是减少程序卡顿。 

开发者学堂课程干货总结——Java 虚拟机原理(一) 

但是后面了if这一段,可以想到刚开始这个循环有问题,因为 byteBuffer.putIntrinsic,最底层是Intrinsic,方法返回的时候没有方法调用。JVM在方法返回以及循环末尾检查是否有Safepoints来看是否要进入GC但是因为这是一个Intrinsic,所以没有到检查点,同样这是一个CountedLoop,也没法去进入检查点因为JVM有个机制,如果这是一个 int作为indexCounted次数的话,为了性能是不会去检查,因为它认为这是有限次的循环,所以不用检查次数。 

这种机制循环里面非常简单,中间有可能因为操作系统原因带来顿,导致循环,基本上没法进入GC,因为线程有进入Safepoints,整个界面都没法进入GC, 夯住很长时间,当时大家觉得很不可思议,但是通过一个很简单方法修好了,就是每隔1000字循环的时候,去调一个Thread.sleep(0) 

刚刚提到,“byteBuffer.put没法出发,Thread.sleep是个JNI返回的时候会检查Safepoints,所以就可以让这个程序能够进入到Safepoints这个代码就不会影响JVM进入到GC了,代码目前还可以从开源软件上看到。 

“-XX:+UseCountedLoopSafepoints 

解决这个问题,还有另一种方式通过一个选项叫“-XX:+UseCountedLoopSafepoints,可以JVM自动在CountedLoop结尾检查这Safepoints当然这带来的副作用CountedLoop末尾都会检查Safepoints,这样就会影响整体性能。 

上一篇:【OpenCV学习】OpenMP并行化实例


下一篇:YAML基础语法