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

简介: Java 虚拟机原理课时1.1——JNI in Java。通过本节课的学习,能够掌握Java 虚拟机原理,学习JNI、类加载器原理、safepoint机制等知识。电子书+视频为同学带来最佳学习效果,文字、课程链接、图谱地址统统为大家放送了哦!

各位同学,开发者学堂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,彼此可以互通。 

image.png 

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

(二)为什么要学习JNI 

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

经典例子,如下图所示,在主函数里面用Selector.open创建一个selectselect方法,这是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数据结构先通过EMCFindClass找到SelectorProvider类,中间有个printf变量叫lock,先通过 GetStaticField获取 field再通过GetStaticObjectFieldcls对象上获取fid就是 lock对象,然后把它打印出来,最后jvm->DestroyJavaVM。详情操作如下图所示: 

image.png 

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

image.png 

平时说,开发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这个程序就可以运行起来了。详情如下图所示: 

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 

首先有这样两个问题: 

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。 

image.png 

(二)JNI与GC 

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

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

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

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

下图所示的二级指针模型还是前面Java调到Native,参数通过jobjecthandle保存使用jobject指向handlehandle指向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性能分析 

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

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

image.png 

 

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

image.png 

 

进一步性能问题,最重要的是performingperforming手段performpublic第二段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 = 0; i < this.fileSize”,每隔一个PACG_SIZEbyteBuffer.put(i, (byte) 0);,这样的话操作系统就会发生缺,把内存真正分配出来,而不只是EMV数据结构。分配出来以后,等到程序真正使用这块内存的时候,就是纯内存IO,不太会触发这种缺页了,可以变得更快,目的是减少程序卡顿。 

image.png 

但是后面了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,这样就会影响整体性能。 

相关文章
|
1月前
|
存储 Oracle Java
java零基础学习者入门课程
本课程为Java零基础入门教程,涵盖环境搭建、变量、运算符、条件循环、数组及面向对象基础,每讲配示例代码与实践建议,助你循序渐进掌握核心知识,轻松迈入Java编程世界。
271 0
|
2月前
|
安全 Java API
Java Web 在线商城项目最新技术实操指南帮助开发者高效完成商城项目开发
本项目基于Spring Boot 3.2与Vue 3构建现代化在线商城,涵盖技术选型、核心功能实现、安全控制与容器化部署,助开发者掌握最新Java Web全栈开发实践。
370 1
|
2月前
|
人工智能 Java 开发者
阿里出手!Java 开发者狂喜!开源 AI Agent 框架 JManus 来了,初次见面就心动~
JManus是阿里开源的Java版OpenManus,基于Spring AI Alibaba框架,助力Java开发者便捷应用AI技术。支持多Agent框架、网页配置、MCP协议及PLAN-ACT模式,可集成多模型,适配阿里云百炼平台与本地ollama。提供Docker与源码部署方式,具备无限上下文处理能力,适用于复杂AI场景。当前仍在完善模型配置等功能,欢迎参与开源共建。
1434 58
阿里出手!Java 开发者狂喜!开源 AI Agent 框架 JManus 来了,初次见面就心动~
|
2月前
|
缓存 Java 开发者
Java 开发者必看!ArrayList 和 LinkedList 的性能厮杀:选错一次,代码慢成蜗牛
本文深入解析了 Java 中 ArrayList 和 LinkedList 的性能差异,揭示了它们在不同操作下的表现。通过对比随机访问、插入、删除等操作的效率,指出 ArrayList 在多数场景下更高效,而 LinkedList 仅在特定情况下表现优异。文章强调选择合适容器对程序性能的重要性,并提供了实用的选择法则。
192 3
|
3月前
|
Java 测试技术 API
2025 年 Java 开发者必知的最新技术实操指南全览
本指南涵盖Java 21+核心实操,详解虚拟线程、Spring Boot 3.3+GraalVM、Jakarta EE 10+MicroProfile 6微服务开发,并提供现代Java开发最佳实践,助力开发者高效构建高性能应用。
649 4
|
3月前
|
人工智能 自然语言处理 Java
面向 Java 开发者:2024 最新技术栈下 Java 与 AI/ML 融合的实操详尽指南
Java与AI/ML融合实践指南:2024技术栈实战 本文提供了Java与AI/ML融合的实操指南,基于2024年最新技术栈(Java 21、DJL 0.27.0、Spring Boot 3.2等)。主要内容包括: 环境配置:详细说明Java 21、Maven依赖和核心技术组件的安装步骤 图像分类服务:通过Spring Boot集成ResNet-50模型,实现REST接口图像分类功能 智能问答系统:展示基于RAG架构的文档处理与向量检索实现 性能优化:利用虚拟线程、GraalVM等新技术提升AI服务性能 文
389 0
|
5月前
|
SQL 人工智能 Java
阿里云百炼开源面向 Java 开发者的 NL2SQL 智能体框架
Spring-ai-alibaba-nl2sql 是析言 GBI 产品在数据问答领域的一次重要开源尝试,专注于 NL2SQL 场景下的核心能力开放。
1731 48
|
6月前
|
Oracle Java 关系型数据库
JVM深入原理(一+二):JVM概述和JVM功能
JVM全称是Java Virtual Machine-Java虚拟机JVM作用:本质上是一个运行在计算机上的程序,职责是运行Java字节码文件,编译为机器码交由计算机运行。
194 0
|
6月前
|
Arthas 存储 Java
JVM深入原理(三+四):JVM组成和JVM字节码文件
目录3. JVM组成3.1. 组成-运行时数据区3.2. 组成-类加载器3.3. 组成-执行引擎3.4. 组成-本地接口4. JVM字节码文件4.1. 字节码文件-组成4.1.1. 组成-基础信息4.1.1.1. 基础信息-魔数4.1.1.2. 基础信息-主副版本号4.1.2. 组成-常量池4.1.3. 组成-方法4.1.3.1. 方法-工作流程4.1.4. 组成-字段4.1.5. 组成-属性4.2. 字节码文件-查看工具4.2.1. javap4.2.2. jclasslib4.2.3. 阿里Arthas
126 0