当前位置:主页 > 查看内容

JNI in Java

发布时间:2021-06-07 00:00| 位朋友查看

简介:一、什么是JNI? 一 什么是JNI (Java Native Interface)? JNI全称是Java Native Interface 顾名思义是Java和Native间的通信桥梁 如下图所示 图的上方是Java世界 下面是Native世界 中间是JNI通信 左边箭头从上往下是Java调用Native的方法 右边是Native调用Java……

一、什么是JNI?

一 什么是JNI (Java Native Interface)?

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

image.png?

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

二 为什么要学习JNI?

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

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

image.png?

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数据结构 先通过EMC的FindClass 找到SelectorProvider类 中间有个printf变量叫lock 先通过?GetStaticField获取 field 再通过GetStaticObjectField从?cls对象上获取fid 就是 lock对象 然后把它打印出来 最后jvm- DestroyJavaVM。详情操作如下图所示 ?

image.png?

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

image.png?

平时所说 开发jvm其实就是开发jvm的动态库 ?“libjvm.so”基本上本身是作为“os”提供出去 好处是非常灵活 可以作为独立应用使用 也可以在别的像cer这样的语言调用 使Java调用Native Native调用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这个程序就可以运行起来了。详情如下图所示 ?

image.png?

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

image.png?

?

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

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

image.png?

?

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

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

image.png?

?

三、JNI与safepoint?

首先有这样两个问题 ?

1、JNI是否会影响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可以并行执行 因为理论上要么执行 要么通过JNI和JNI交互 所有的跟JNI相关的数据结构都可以被管理。然后Native还可以去切换到JVM状态 这是非常关键的状态 这个状态不能发生GC 不用关心。?

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

image.png?

二 JNI与GC?

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

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

void?ReleasePrimitiveArrayCritical(JNIEnv?*env,?jarray?array, void *carray,?jint?mode);?

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

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

image.png?

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机制 主要为性能而生。?

image.png?

image.png?

?

二 Intrinsic性能分析?

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

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

image.png?

?

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

image.png?

?

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

image.png?

下面是“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 ?i? ?this.fileSize” 每隔一个PACG_SIZE去“byteBuffer.put(i, (byte) 0)”; 这样的话 操作系统就会发生缺页 把内存真正分配出来 而不只是EMV数据结构。分配出来以后 等到程序真正使用这块内存的时候 就是纯内存IO 不太会触发这种缺页了 可以变得更快 目的是减少程序卡顿。?

image.png?

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

这种机制循环里面非常简单 中间有可能因为操作系统原因带来卡顿 导致循环 基本上没法进入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 这样就会影响整体性能。?


本文转自网络,原文链接:https://developer.aliyun.com/article/784537
本站部分内容转载于网络,版权归原作者所有,转载之目的在于传播更多优秀技术内容,如有侵权请联系QQ/微信:153890879删除,谢谢!

推荐图文


随机推荐