执行引擎
执行引擎(Execution Engine)概述
1、执行引擎属于 JVM 的下层
2、包括:解释器、及时编译器、垃圾回收器
3、执行引擎是 JVM 核心的组成部分之一
4、作用:将字节码指令解释 / 编译为对应平台上的本地机器指令,即 JVM 中的执行引擎将高级语言翻译为机器语言
执行引擎的工作流程
1、执行的字节码指令完全依赖于程序计数寄存器
2、每当执行完一项指令操作后,程序计数寄存器就会更新下一条需要被执行的指令地址
3、方法在执行的过程中,执行引擎可能会通过存储在局部变量表中的对象引用,准确定位到存储在 Java 堆区中的对象实例信息,以及通过对象头中的元数据指针,定位到目标对象的类型信息
4、所有 JVM 的执行引擎输入、输出都是一致的:输入的是字节码二进制流,处理过程是字节码解析执行的等效过程,输出的是执行过程
Java 代码编译和执行过程
1、大部分的程序代码,转换成物理机的目标代码,或虚拟机能执行的指令集之前,需要经过以下各个步骤
(1)解释器:当 JVM 启动时,根据预定义的规范,对字节码采用逐行解释的方式执行,将每条字节码文件中的内容,翻译为对应平台的本地机器指令执行
(2)JIT 编译器(Just In Time Compiler):虚拟机将源代码直接编译成和本地机器平台相关的机器语言
2、Java 源码编译器(前端编译器)编译 Java 代码,流程图如下
3、JVM 执行引擎(后端编译器)执行 Java 字节码,流程图如下
Java 是半编译半解释型语言
1、JDK 1.0,Java 语言定位为解释执行
2、Java 发展出可以直接生成本地代码的编译器
3、现在 JVM 在执行 Java 代码时,通常都会将解释执行与编译执行二者结合起来进行
机器码
1、机器指令码 / 机器语言:各种用二进制编码方式表示的指令
2、机器语言虽然能够被计算机理解和接受,但和人们的语言差别太大,不易被人们理解和记忆,并且用它编程容易出差错
3、用它编写的程序一经输入计算机,CPU 直接读取运行,因此和其他语言编的程序相比,执行速度最快
4、机器指令与 CPU 紧密相关,所以不同种类的 CPU 所对应的机器指令不同
指令
1、机器码是由 0 和 1 组成的二进制序列,可读性太差
2、指令:把机器码中特定的 0 和 1 序列,简化成对应的指令(一般为英文简写,如:mov,inc 等),可读性稍好
3、由于不同的硬件平台,执行同一个操作,对应的机器码可能不同,所以不同的硬件平台的同一种指令,对应的机器码也可能不同
指令集
1、不同的硬件平台,各自支持的指令存在差别
2、每个平台所支持的指令,称之为对应平台的指令集
(1)x86 指令集,对应 x86 架构平台
(2)ARM 指令集,对应 ARM 架构平台
汇编语言
1、因为指令的可读性还是太差,所以产生汇编语言
2、在汇编语言中,用助记符(Mnemonics)代替机器指令的操作码,用地址符号(Symbol)或标号(Label)代替指令或操作数的地址
3、在不同的硬件平台,汇编语言对应不同的机器语言指令集,通过汇编过程转换成机器指令
4、由于计算机只认识指令码,所以用汇编语言编写的程序,必须翻译成机器指令码,计算机才能识别和执行
高级语言
1、高级语言比机器语言、汇编语言更接近人的语言
2、解释程序、编译程序:计算机执行高级语言编写的程序时,需要把程序解释和编译成机器指令码
3、高级语言不是直接翻译成机器指令,而是翻译成汇编语言码
4、如:C / C++ 源程序执行过程
(1)编译过程分成两个阶段:编译、汇编
(2)编译过程:读取源程序(字符流),对之进行词法和语法的分析,将高级语言指令转换为功能等效的汇编代码
(3)汇编过程:把汇编语言代码,翻译成目标机器指令
字节码
1、字节码是一种中间状态(中间码)的二进制代码(文件),比机器码更抽象,需要直译器转译后才能成为机器码
2、目的:实现特定软件运行和软件环境,与硬件环境无关
3、实现方式:编译器、虚拟机器
(1)编译器:将源码编译成字节码
(2)特定平台上的虚拟机器:将字节码转译为可以直接执行的指令
4、字节码典型应用:Java bytecode
解释器
1、Java 程序的跨平台特性,避免采用静态编译的方式,直接生成本地机器指令,从而实现解释器在运行时,采用逐行解释字节码执行程序
2、工作机制
(1)运行时,将字节码文件中的内容,翻译为对应平台的本地机器指令执行
(2)当解释执行完成一条字节码指令后,再根据程序计数寄存器中,记录下一条需要被执行的字节码指令,执行解释操作
3、分类
(1)字节码解释器:在执行时,通过纯软件代码,模拟字节码的执行,效率非常低下
(2)模板解释器:将每一条字节码和一个模板函数相关联,模板函数中直接产生这条字节码执行时的机器码,提高解释器的性能
4、在 HotSpot VM 中,解释器主要由 Interpreter 模块和 Code 模块构成
(1)Interpreter 模块:实现解释器的核心功能
(2)Code 模块:管理 HotSpot VM 在运行时生成的本地机器指令
5、解释器在设计和实现上非常简单,但执行效率低下
(1)基于解释器执行,比如:Python、Perl、Ruby、Java 等
(2)JVM 平台支持即时编译的技术
(3)即时编译的目的:避免函数被解释执行,而是将整个函数体编译成为机器码,每次函数执行时,只执行编译后的机器码即可,可以使执行效率大幅度提升
即时编译器
1、Just In Time Compiler
2、Java 执行代码分类
(1)解释执行:将源代码编译成字节码文件,然后在运行时,通过解释器将字节码文件转为机器码执行
(2)编译执行:直接编译成机器码,但不同机器上编译的机器码不同,而字节码是可以跨平台的,现代虚拟机为提高执行效率,会使用即时编译技术,将方法编译成机器码后再执行
2、HotSpot VM 采用解释器与即时编译器并存架构
(1)在 JVM 运行时,解释器和即时编译器能够相互协作,各自取长补短,选择最合适的方式,权衡编译本地代码的时间和直接解释执行代码的时间
(2)当程序启动后,解释器可以马上发挥作用,省去编译的时间,立即执行
(3)编译器启动时,把代码编译成本地代码,需要一定的执行时间,但编译为本地代码后,执行效率高
(4)JRockit VM 内部不包含解释器,字节码全部都依靠即时编译器编译后执行,对于服务端应用,启动时间并非是关注重点
3、HotSpot JVM 执行方式
(1)当 JVM 启动时,解释器可以首先发挥作用,而不必等待即时编译器全部编译完成后再执行,省去许多不必要的编译时间
(2)随着时间的推移,编译器将更多代码编译成本地代码,获得更高的执行效率
(3)解释执行在编译器进行激进优化不成立时,作为编译器的备用
线上环境
1、热机状态,使用解释执行
2、冷机状态,使用编译执行
3、机器在热机状态可以承受的负载要大于冷机状态
4、如果以热机状态时的流量进行切流,可能使处于冷机状态的服务器因无法承载流量而假死
5、在生产环境发布过程中,以分批的方式进行发布,根据机器数量划分成多个批次,每个批次的机器数至多占到整个集群的 1 / 8
Java 编译期
1、前端编译器把 .java 文件转变成 .class 文件的过程
2、后端运行期编译器(JIT 编译器,Just In Time Compiler)把字节码转变成机器码的过程
3、静态提前编译器(AOT 编译器,Ahead of Time Compiler)直接把 .java 文件编译成本地机器代码的过程
Java 编译器
1、前端编译器:Sun 的 Javac、Eclipse JDT 中的增量式编译器(ECJ)
2、JIT 编译器:HotSpot VM 的 C1、C2 编译器、Graal 编译器
3、AOT 编译器:GNU Compiler for the Java(GCJ)、Excelsior JET
热点代码
1、热点代码:需要被即时编译器编译为本地代码的字节码
(1)被多次调用的方法
(2)被多次执行的循环体
2、根据代码被调用执行的频率确定,是否需要启动 JIT 编译器,将字节码直接编译为对应平台的本地机器指令
3、JIT 编译器在运行时,会深度优化频繁被调用的热点代码,将其直接编译为对应平台的本地机器指令,以此提升 Java 程序的执行性能
4、对于两种热点代码,编译的目标对象都是整个方法体,而不会是单独的循环体
(1)被多次调用的方法:由于是依靠方法调用触发的编译,那编译器理所当然地会以整个方法作为编译对象,这种编译也是虚拟机中标准的即时编译方式
(2)被多次执行的循环体:尽管编译动作是由循环体所触发的,热点只是方法的一部分,但编译器依然必须以整个方法作为编译对象,只是执行入口(从方法第几条字节码指令开始执行)会稍有不同,编译时会传入执行入口点字节码序号(Byte Code Index,BCI)。这种编译方式因为编译发生在方法执行的过程中,因此被称为“栈上替换”(On Stack Replacement,OSR),即方法的栈帧还在栈上,方法就被替换了
热点探测
1、要知道某段代码是不是热点代码,是不是需要触发即时编译,这个行为称为“热点探测”(HotSpot Code Detection)
2、进行热点探测并不一定要知道方法具体被调用了多少次
3、基于采样的热点探测(Sample Based Hot Spot Code Detection)
(1)采用这种方法的虚拟机会周期性地检查各个线程的调用栈顶,如果发现某个(或某些)方法经常出现在栈顶,那这个方法就是“热点方法”
(2)优点:实现简单高效,还可以很容易地获取方法调用关系(将调用堆栈展开即可)
(3)缺点:很难精确地确认一个方法的热度,容易因为受到线程阻塞或别的外界因素的影响而扰乱热点探测
4、基于计数器的热点探测(Counter Based Hot Spot Code Detection)
(1)采用这种方法的虚拟机会为每个方法(甚至是代码块)建立计数器,统计方法的执行次数,如果执行次数超过一定的阈值就认为它是“热点方法”
(2)这种统计方法实现起来要麻烦一些,需要为每个方法建立并维护计数器,而且不能直接获取到方法的调用关系
(3)但是它的统计结果相对来说更加精确严谨
5、HotSpot VM 所采用的热点探测方式,基于计数器的热点探测
6、HotSpot VM 将为每一个方法都建立 2 个不同类型的计数器
(1)方法调用计数器(Invocation Counter):统计方法的调用次数
(2)回边计数器(Back Edge Counter):统计循环体执行的循环次数,“回边”指在循环边界往回跳转
(3)当虚拟机运行参数确定的前提下,这两个计数器都有一个明确的阈值,计数器阈值一旦溢出,就会触发即时编译
方法调用计数器
1、统计方法的调用次数
2、阈值
(1)设置 -XX:CompileThreshold 虚拟机参数
(2)超过阈值,触发 JIT 编译
3、默认阀值
(1)在 Client 模式下是 1500 次
(2)在 Server 模式下是 10000 次
4、过程
(1)当一个方法被调用时,会先检查该方法是否存在被 JIT 编译过的版本
(2)如果存在,则优先使用编译后的本地代码来执行
(3)如果不存在已被编译过的版本,则将此方法的调用计数器值加 1,然后判断方法调用计数器与回边计数器值之和,是否超过方法调用计数器的阀值
(4)如果已超过阈值,则将会向即时编译器提交一个该方法的代码编译请求
(5)如果没有做过任何设置,执行引擎默认不会同步等待编译请求完成,而是继续进入解释器按照解释方式执行字节码,直到提交的请求被即时编译器编译完成
(6)当编译工作完成后,这个方法的调用入口地址就会被系统自动改写成新值,下一次调用该方法时就会使用已编译的版本了
热点衰减
1、在默认设置下,方法调用计数器统计的并不是方法被调用的绝对次数,而是一个相对的执行频率,即一段时间之内方法被调用的次数
2、当超过一定的时间限度,如果方法的调用次数仍然不足以让它提交给即时编译器编译,则该方法的调用计数器会被减少一半
(1)过程:方法调用计数器热度衰减(Counter Decay)
(2)衰减时间:该方法统计的半衰周期(Counter Half Life Time)
3、执行时机:在虚拟机进行垃圾收集时顺便进行
4、设置
(1)默认开启热度衰减
(2)-XX:-UseCounterDecay,关闭热度衰减,让方法计数器统计方法调用的绝对次数,只要系统运行时间足够长,绝大部分方法都会被编译成本地代码
(2)-XX:CounterHalfLifeTime:设置半衰周期的时间,单位是秒
回边计数器
1、作用:统计一个方法中循环体代码执行的次数
2、回边(Back Edge)
(1)在字节码中遇到控制流,向后跳转的指令
(2)回边的次数而不是循环次数,因为并非所有的循环都是回边,如空循环实际上就可以视为自己跳转到自己的过程,因此并不算作控制流向后跳转,也不会被回边计数器统计
3、目的:触发栈上替换 / OSR(On Stack Replacement)编译
4、回边计数器的阈值
(1)虽然 HotSpot 虚拟机也提供了一个类似于方法调用计数器阈值 -XX:CompileThreshold 的参数 -XX:BackEdgeThreshold 供用户设置,但是当前的 HotSpot 虚拟机实际上并未使用此参数
(2)必须设置另外一个参数 -XX:OnStackReplacePercentage 来间接调整回边计数器的阈值
5、虚拟机运行在 Client 模式下
(1)回边计数器阈值计算公式为:方法调用计数器阈值 (-XX:CompileThreshold) * OSR 比率 (-XX:OnStackReplacePercentage) / 100
(2)其中 -XX:OnStackReplacePercentage 默认值为 933,如果都取默认值,那 Client 模式虚拟机的回边计数器的阈值为 13995
6、虚拟机运行在 Server 模式下
(1)回边计数器阈值的计算公式为:方法调用计数器阈值 (-XX:CompileThreshold) * ( OSR 比率 (-XX:OnStackReplacePercentage) - 解释器监控比率 (-XX:InterpreterProfilePercentage)) / 100
(2)其中 -XX:OnStackReplacePercentage 默认值为 140,-XX:InterpreterProfilePercentage 默认值为 33,如果都取默认值,那服务端模式虚拟机回边计数器的阈值为 10700
7、执行过程
(1)当解释器遇到一条回边指令时,会先查找将要执行的代码片段是否有已经编译好的版本
(2)如果有的话,它将会优先执行已编译的代码
(3)否则就把回边计数器的值加一,然后判断方法调用计数器与回边计数器值之和是否超过回边计数器的阈值
(4)当超过阈值的时候,将会提交一个栈上替换编译请求,并且把回边计数器的值稍微降低一些,以便继续在解释器中执行循环,等待编译器输出编译结果
8、与方法计数器不同,回边计数器没有计数热度衰减的过程
(1)因此这个计数器统计的就是该方法循环执行的绝对次数
(2)当计数器溢出的时候,它还会把方法计数器的值也调整到溢出状态,这样下次再进入该方法的时候就会执行标准编译过程
后台执行编译
1、在默认条件下,无论是方法调用产生的标准编译请求,还是栈上替换编译请求,虚拟机在编译器还未完成编译之前,都仍然将按照解释方式继续执行代码,而编译动作则在后台的编译线程中进行
2、用户可以通过参数 -XX:-BackgroundCompilation 来禁止后台编译,后台编译被禁止后,当达到触发即时编译的条件时,执行线程向虚拟机提交编译请求以后将会一直阻塞等待,直到编译过程完成再开始执行编译器输出的本地代码
3、服务端编译器和客户端编译器的编译过程是有所差别的
4、对于客户端编译器来说,它是一个相对简单快速的三段式编译器,主要的关注点在于局部性的优化,而放弃了许多耗时较长的全局优化手段
(1)在第一个阶段,一个平台独立的前端将字节码构造成一种高级中间代码表示(High-LevelIntermediate Representation,HIR,即与目标机器指令集无关的中间表示)。HIR 使用静态单分配(Static Single Assignment,SSA)的形式来代表代码值,这可以使得一些在 HIR 的构造过程之中和之后进行的优化动作更容易实现。在此之前编译器已经会在字节码上完成一部分基础优化,如方法内联、常量传播等优化将会在字节码被构造成 HIR 之前完成
(2)在第二个阶段,一个平台相关的后端从 HIR 中产生低级中间代码表示(Low-Level Intermediate Representation,LIR,即与目标机器指令集相关的中间表示),而在此之前会在 HIR 上完成另外一些优化,如空值检查消除、范围检查消除等,以便让 HIR 达到更高效的代码表示形式
(3)最后的阶段是在平台相关的后端使用线性扫描算法(Linear Scan Register Allocation)在 LIR 上分配寄存器,并在 LIR 上做窥孔(Peephole)优化,然后产生机器代码
HotSpotVM 可以设置程序执行方法
1、默认情况下 HotSpot VM 采用解释器与即时编译器并存的架构
2、可以根据具体的应用场景,通过命令显式地为 JVM 指定在运行时,完全采用解释器执行,还是完全采用即时编译器执行
(1)-Xint:完全采用解释器模式执行程序
(2)-Xcomp:完全采用即时编译器模式执行程序,如果即时编译出现问题,解释器会介入执行
(3)-Xmixed:采用解释器 + 即时编译器的混合模式共同执行程序
HotSpotVM 中 JIT 分类
1、JIT 两种编译器:C1、C2
2、HotSpot VM 中内嵌两个 JIT 编译器:Client Compiler、Server Compiler,简称为 C1 编译器、C2 编译器
3、可以显式指定 JVM 在运行时使用的即时编译器
(1)-client:指定 JVM 运行在 Client 模式下,并使用 C1 编译器,C1 对字节码进行简单和可靠的优化,耗时短,以达到更快的编译速度
(2)-server:指定 JVM 运行在 Server 模式下,并使用 C2 编译器,C2 对字节码进行耗时较长的优化,以及激进优化,但优化的代码执行效率更高
4、分层编译(Tiered Compilation)策略
(1)程序解释执行(不开启性能监控)可以触发 C1 编译,将字节码编译成机器码,可以进行简单优化,也可以加上性能监控,C2 编译会根据性能监控信息进行激进优化
(2)在 Java 7 版本之后,显式指定命令 -server 时,默认将会开启分层编译策略,由 C1 和 C2 相互协作共同来执行编译任务
5、C1 优化策略
(1)方法内联:将引用的函数代码,编译到引用点处,减少栈帧生成、参数传递、跳转过程
(2)去虚拟化:对唯一实现类进行内联
(3)冗余消除:在运行期间,折叠不会执行的代码
6、C2 优化策略
(1)全局层面优化
(2)逃逸分析(不成熟)是优化的基础
(3)分离对象 / 标量替换:用标量值代替聚合对象的属性值
(3)栈上分配:对于未逃逸对象,分配对象在栈而不是堆
(4)同步省略 / 锁消除:清除同步操作,通常指 synchronized
Graal 编译器
1、JDK 10 起,HotSpot 加入该即时编译器
2、使用激活参数:-XX:+UnlockExperimentalvMOptions -XX:+UseJVMCICompile
3、Java虚拟机编译器接口(Java-Level JVM CompilerInterface,JVMCI)
(1)响应 HotSpot 的编译请求,并将该请求分发给 Java 实现的即时编译器
(2)允许编译器访问 HotSpot 中与即时编译相关的数据结构,包括类、字段、方法及其性能监控数据等,并提供了一组这些数据结构在 Java 语言层面的抽象表示
(3)提供 HotSpot 代码缓存(Code Cache)的 Java 端抽象表示,允许编译器部署编译完成的二进制机器码
(4)可以把一个在 HotSpot 虚拟机外部的、用 Java 语言实现的即时编译器(不局限于 Graal)集成到 HotSpot 中,响应 HotSpot 发出的最顶层的编译请求,并将编译后的二进制代码部署到 HotSpot 的代码缓存中
(5)单独使用(3),又可以绕开 HotSpot 的即时编译系统,让该编译器直接为应用的类库编译出二进制机器码,将该编译器当作一个提前编译器去使用(如 Jaotc)
4、Graal 编译器在设计之初就刻意采用了与 HotSpot 服务端编译器一致(略有差异但已经非常接近)的中间表示形式,也即是被称为 Sea-of-Nodes 的中间表示,或者与其等价的被称为理想图(Ideal Graph,在代码中称为 Structured Graph)的程序依赖图(Program Dependence Graph,PDG)形式
AOT 编译器
1、JDK 9 引入静态提前编译器(AOT 编译器,Ahead of Time Compiler)
2、Java 9 引入实验性 AOT 编译工具:jaotc,借助 Graal 编译器,将所输入 Java 类文件转换为机器码,并存放至生成的动态共享库之中
3、AOT 编译,与即时编译相对立
(1)即时编译:在程序的运行过程中,将字节码转换为可在硬件上直接运行的机器码,并部署至托管环境中的过程
(2)AOT 编译:在程序运行之前,将字节码转换为机器码的过程
4、优点:JVM 加载已经预编译成二进制库,可以直接执行,不必等待及时编译器的预热
5、缺点
(1)破坏 Java“一次编译,到处运行”理念,必须为每个不同的硬件,OS 编译对应的发行包
(2)降低 Java 链接过程的动态性,加载的代码在编译器就必须全部已知
(3)需要继续优化,最初只支持 Linux X64 java base
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】凌霞软件回馈社区,博客园 & 1Panel & Halo 联合会员上线
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】博客园社区专享云产品让利特惠,阿里云新客6.5折上折
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 微软正式发布.NET 10 Preview 1:开启下一代开发框架新篇章
· 没有源码,如何修改代码逻辑?
· PowerShell开发游戏 · 打蜜蜂
· 在鹅厂做java开发是什么体验
· WPF到Web的无缝过渡:英雄联盟客户端的OpenSilver迁移实战