执行引擎、JIT、逃逸分析
JVM中的执行引擎是什么?
在Java虚拟机(JVM)中,执行引擎(Execution Engine)是负责执行Java字节码的核心组件。执行引擎的作用是将Java字节码转换成计算机可以执行的机器码,并实际执行这些机器码。以下是JVM执行引擎的主要职责和组成部分:
主要职责:
- 1.加载和验证字节码:执行引擎确保加载的类文件符合JVM规范,并进行验证
- 2.执行字节码:执行引擎逐条执行Java字节码指令
- 3.优化执行:在运行时,执行引擎可能会对字节码进行优化以提高性能
- 4.内存管理:执行引擎负责管理JVM运行时的内存,包括堆(Heap)、栈(Stacks)、方法区(Method Area)等
执行引擎的组成部分:
解释器(Interpreter):
- 1.解释器是执行引擎的一部分,它逐条读取和执行Java字节码
- 2.解释执行的特点是启动速度块,但执行速度相对较慢,因为它需要逐条解释和执行字节码
即时编译器(Just-In-Time Compiler, JIT)
- 1.JIT编译器将Java字节码编译成本地机器码,这样可以直接在硬件上执行,从而提高执行效率
- 2.JIT编译器在运行时进行编译,可以针对程序的运行特性进行优化
垃圾回收器(Garbage Collector, GC)
- 1.虽然垃圾回收器主要负责内存管理,但它也是执行引擎的一部分,因为它在执行过程中负责清理不再使用的对象
本地方法接口(Natvie Method Interface, JNI)
- 1.JNI允许Java代码调用其他语言编写的本地(Native)方法,这些方法通常用C/C++编写
本地方法库(Native Method Libraries)
- 1.本地方法库时执行引擎调用的本地方法所在的库,这些库提供了Java程序可以调用的本地函数
执行引擎的工作流程大致如下:
- 1.类加载器将.class文件加载到JVM中
- 2.字节码验证其确保加载的字节码是有效的
- 3.解释器开始逐条执行字节码
- 4.在运行过程中,JIT编译器可能会识别出热点代码(执行频率高的代码),并将这些字节码编译成本地机器码以加速执行
- 5.如果程序调用了本地方法,JNI会负责调用相应的本地方法库中的函数
执行引擎的设计和实现因不同的JVM实现而异,但他们都遵循Java虚拟机规范,以确保Java程序在不同的JVM上能够一致地运行
Java为什么是半编译半解释型语言?
分成两个角度来看
- 1.触发JIT之前javac编译,java运行
- 2.触发JIT之后,运行期即时编译(C1、C2)+解释执行(模板解释器比字节码解释器高效很多)
JVM的三种执行模式
解释模式
- 1.通过解释器(Bytecode Interpreter)解释执行
特点是:启动快(不需要编译),执行慢
可通过:-Xint参数执行为纯解释模式 - 2.编译模式
由JIT(Just In Time Compiler)编译为本地代码(C语言代码)主席那个
特点:启动慢(编译过程慢),执行快
可通过-Xcomp参数指定为纯编译模式 - 3.混合模式
混合使用解释器(Bytecode Compiler) + 热点代码编译(Just In Time Compiler)
起始阶段采用解释执行
热点代码检测(HotSpot),默认-XX:CompileThreshold=10000
多次被调用的方法(方法计数器:监测方法执行频率)
多次被调用的循环(循环计数器,监测循环执行频率)
对热点代码进行编译
默认采用这种模式,可通过-Xmixed指定
在JDK9及更高的版本中,JVM引入了AOT(Ahead-Of-Time)编译器。AOT编译允许在程序执行之前将Java字节码提前编译为机器码,从而避免或减少运行时的解释和即时编译。这种方式与传统的JIT不同,因为它在程序启动之前就完成了编译
AOT(Ahead Of Time)
提前编译是相对于即时编译的概念,提前编译能带来的最大好处是Java虚拟机加载这些已经预编译成二进制库之后就能够直接调用,而无须再等待即时编译器在运行时将其编译成二进制机器码。理论上,提前编译可以减少即时编译带来的预热时间,减少Java应用长期给人带来的“第一次运行慢"的不良体验,可以放心地进行很多全程序的分析行为,可以使用时间压力更大的优化措施。但是提前编译的坏处也很明显,它破坏了Java"—次编写,到处运行"的承诺,必须为每个不同的硬件、操作系统去编译对应的发行包;也显著降低了Java链接过程的动态性,必须要求加载的代码在编译期就是全部已知的,而不能在运行期才确定,否则就只能舍弃掉己经提前编译好的版本,退回到原来的即时编译执行状态。
AOT的优点
在程序运行前编译,可以避免在运行时的编译性能消耗和内存消耗
可以在程序运行初期就达到最高性能,程序启动速度快
运行产物只有机器码,打包体积小
AOT的缺点
由于是静态提前编译,不能根据硬件情况或程序运行情况择优选择机器指令序列,理论峰值性能不如JIT
没有动态能力
同一份产物不能跨平台运行
参考文章https://cloud.tencent.com/developer/article/2228910
Java被称为"半编译半解释型"语言
主要是因为它的编译和执行过程结合了编译和解释两种方式。这个特点使得Java既可以保持一定的执行效率,又具备良好的跨平台特性。
具体来说,Java的编译和执行过程如下:
- 1.源代码编译:Java程序员编写的Javaa源代码文件(.java文件)首先由Java编译器(javac)编译,生成字节码文件(.class文件).这个过程类似于其他编译型语言的编译步骤。
字节码是一种中间形式的代码,它并不是机器码,而是一种中间表示形式,主要是为Java虚拟机(JVM)准备的 - 2.字节码解释:编译后的字节码不是直接由操作系统执行的,而是由Java虚拟机(JVM)解释执行。JVM是一个虚拟的计算机,负责将字节码解释为特定平台的机器码,从而使得
Java程序得以运行。这种解释的方式使得Java程序可以在不同的操作系统上运行,只要改系统上有相应的JVM - 3.即时编译(JIT):为了提高程序执行的效率,JVM还采用了即使编译技术(Just-In-Time Compiler, JIT)。当JVM执行字节码时,它会将某些频繁执行的字节码动态编译成机器码,
以提高执行效率。这种即时编译进一步缩短了程序的执行时间,接近于纯编译型语言的性能
总结来说,Java的"半编译半解释"指的是:
- 1.编译部分:Java源代码被编译成字节码,这一步类似于编译型语言的过程
- 2.解释部分:字节码由JVM解释执行,或在运行时通过JIT编译为机器码,这使得Java具备跨平台的能力,同事保证了较高的执行效率
这种设计使得Java既可以保证跨平台型,又能在执行效率上达到较好的平衡
两种解释器的底层实现,JVM中目前来说有两种解释器
字节码解释器
做的事情是:Java字节码->C++代码->硬编码,比如说一条_new指令,字节码解释器bytecodeInterpreter
C++代码中有很多跟new指令无关的才能到目标代码,好比你要执行一条字节码指令_new,字节码解释器需要执行很多操作,比如说要读取字节码文件对应索引的指令,才能到真正这条字节码需要做的事情,比较繁琐。一个字节码指令占一个字节,2^8-1=255,所以最多只能有255个
模板解释器
做的事情是Java字节码->硬编码
void TemplateTable::_new() {
transition(vtos, atos);
Label slow_case;
Label done;
Label initialize_header;
Label initialize_object; // including clearing the fields
Register RallocatedObject = Otos_i;
Register RinstanceKlass = O1;
Register Roffset = O3;
Register Rscratch = O4;
__ get_2_byte_integer_at_bcp(1, Rscratch, Roffset, InterpreterMacroAssembler::Unsigned);
__ get_cpool_and_tags(Rscratch, G3_scratch);
// make sure the class we're about to instantiate has been resolved
// This is done before loading InstanceKlass to be consistent with the order
// how Constant Pool is updated (see ConstantPool::klass_at_put)
__ add(G3_scratch, Array<u1>::base_offset_in_bytes(), G3_scratch);
__ ldub(G3_scratch, Roffset, G3_scratch);
__ cmp(G3_scratch, JVM_CONSTANT_Class);
__ br(Assembler::notEqual, false, Assembler::pn, slow_case);
……
内部维护了一个执行流数组RunStream[] arr = new RunStream[255];
arr[187] = {
0x55, // pushq %rbp
0x48,0x89,0xe5 // movq %rsp, %rbp
0xb8, 0x0b, 0x00,0x00,0x00 // movl $0xb, %eax
0x5d, // popq %rbp
0xc3 // retq
}
这种行为就叫硬编码编织,比C++执行效率要高,怎么理解呢?编织技术一种精细化的构造,使用的内存消耗小于原生的内存分配.
JIT底层实现(找到硬编码)
- 1.申请一块内存(可读可写可执行权限),线性地址用来存储代码
- 2.调用函数指针
- 3.硬编码写入
malloc申请的内存区域是可读可写的。模板解释器可以省区哪些没有意义的汇编代码
JVM三种运行模式
JIT为什么能提升性能呢?原因在于运行期的热点代码编译与缓存
JVM中有三种两种及时编译器,就诞生了三种运行模式
- 1.-Xint:纯字节码解释器模式
- 2.-Xcomp:纯模板解释器模式
- 3.-Xmixed 字节码解释器+模板解释器模式(默认)
-Xint
-Xcomp
-Xmixed
为什么没有用纯编译模式?
首次启动的时候要生成所有执行流,可能是担心生成执行流的时间比较长,用mixed的话,可以字节码解释器一边运行一边用热点代码编译
两种即时编译器(在虚拟机中习惯将Client Compiler称为C1,将Server Compiler称为C2)
jdk6以前是没有混合编译的,后来根据两种比那一起的使用场景组合起来使用进一步提升性能
-
1.C1编译器
-client模式启动,默认启动的是C1编译器。它的特点如下:
1.需要收集的数据比较少,即达到触发即时编译的条件比较宽松
2.自带的编译优化优化的点比较少
3.编译时和C2相比,没那么耗CPU,带来的结果是编译后生成的代码执行效率比C2低 -
2.C2编译器
-server模式启动。有哪些特点呢?
1.需要收集的数据较多
2.编译时很耗CPu
3.编译优化的点较多
4.编译生成的代码执行效率高 -
3.混合编译。
目前的-server模式启动,已经不是纯粹只使用C2.程序运行初期因为产生的数据较少,这时候执行C1编译,程序执行一段时间后,收集到足够的数据,执行C2编译器。早期触发C1,后面再触发C2,一个代码块有很多的执行流。热点代码存储再方法区元空间opcodecache热点代码缓存区域
C1和C2和GCC编译优化的-O0、O1、O2、O3类似
Client Compiler架构设计
对于Client Compiler来说,它是一个简单快速的三段式编译器,主要的关注点在于局部性的哟话,而放弃了许多耗时较长的全局优化手段。
- 1.在第一个阶段,一个平台独立的前端将字节码构造成一种高级中间代码标识(High-Level Intermediate Representation, HIR).HIR使用静态单分配(Static Single Assignment, SSA)的形式来代表代码值,这可以使得一些在HIR的构造过程之中和之后进行的优化动作更容易实现。在此之前编译器会在字节码上完成一部分基础优化,如方法内联、常量传播等优化将会在字节码被构造成HIR之前完成。
- 2.在第二个阶段,一个平台相关的后端从IHR中产生低级中间代码标识(Low-Level Intermediate Representation, LIR),而再次之前会在HIR上完成另外一些优化,如空值检查消除、范围检查消除等,以便在HIR达到更高效的代码表示形式
- 3.最后阶段是在平台相关的后端使用线性扫描算法(LInear Scan Register Allocation)在LIR上做窥孔(Peephole)优化,然后产生机器代码
Server Compiler
Server Compiler则是专门面向服务端的典型应用并为服务端的性能配置特别调整过的编译器,也是一个充分优化过的高级编译器,几乎能达到GNU C++编译器使用-O2参数的优化强度,它会执行所有经典的优化动作,如无用代码消除(Dead Code Elimination)、循环展开(Loop Unrolling)、循环表达式外提(Loop Expressin Hoisting)、消除公共子表达式(Common Subexpression Elimination)、常量传播(Constant Propagation)、基本块重排序(Basic Block Reordering)等,还会实施一些与Java语言特性密切相关的优化技术,如范围检查消除(Range Check Elimination)、空值检查消除(Null Check Elimination,不过并非所有的空值检查消除都是依赖编译器优化的,有一些是在代码运行过程中自动优化了)等。另外,还可能根据解释器或Client Compiler提供的性能监控信息,进行一些不稳定的基金优化,如守护内联(Guarded Inlining)、分支频率预测(Branch Frequency Prediction)等。
Server Compiler的寄存器分配是一个全局着色分配器,它可以充分利用某些处理器架构(如RISC)上的大寄存器集合。以即时编译的标准来看,Server Compiler无疑是比较缓慢的。但它的编译速度依然远远超过传统的静态优化编译器,而且它相对于Client Compiler编译输出的代码质量有所提高,可以减少本地代码的执行时间,从而抵消了额外的编译时间开销,所以也有很多非服务端的应用选择使用Server模式的虚拟机运行
JIT编译过程本来就是一个虚拟机中最体现技术水平也是最复杂的部分,不可能以较短的篇幅就介绍得很详细,另外,这个过程对Java开发来说是透明的,程序员平时无法感知它的存在。