写点什么

☕️【Java 技术之旅】深入学习 JIT 编译器实现机制(原理篇)

发布于: 2021 年 06 月 01 日
☕️【Java技术之旅】深入学习JIT编译器实现机制(原理篇)

前提概要

解释器

Java 程序最初是通过解释器(Interpreter)进行解释执行的,当虚拟机发现某个方法或代码块的运行特别频繁的时候,就会把这些代码认定为“热点代码”(hotspot code)。正因为如此,我们的 hotspot 的虚拟机就是因此而得名。

解释器优点

  • (占用空间较少)解释执行占用更小的内存空间

  • (启动和首次执行速度较快)当程序需要迅速启动的时候,解释器可以首先发挥作用,省去了编译的时间,立即执行

  • (提高动态性和移植性)当处于程序的动态效果下,如果预先编译好所有相关的静态本地代码后,就无法实现动态化扩展,以及提高移植到其他计算机平台架构下的能力



编译器

为了提高热点代码的执行效率,在运行时,即时编译器(Just In Time Compiler,下文称 JIT 编译器 )会把这些代码编译成与本地平台相关的机器码,并进行各种层次的优化。

编译器优点

  • (提高运行速度)在程序运行时,随着时间的推移,编译器逐渐发挥作用,把越来越多的代码编译成本地代码之后,可以获得更高的执行效率。

  • (逆转优化)同时,当编译器进行的激进优化失败的时候,还可以进行逆优化来恢复到解释执行的状态。


因此,整个虚拟机执行架构中,解释器与编译器经常配合工作,如下图所示。


解释器与编译器并存的架构(流程)

  1. 如果 Java 程序需要迅速启动和执行时,或者只是执行一次,解释器可首先发挥作用,省去编译时间,立即执行程序运行后,随着时间推移,JIT 编译器逐渐发挥作用,把越来越多的代码编译成本地代码后,可获取更高执行效率。

  2. 程序运行环境中内存资源限制较大(如部分嵌入式系统中),可使用解释执行节约内存,反之可使用 JIT 编译执行提升效率

  3. 解释器还可作为 JIT 编译器激进优化时的一个“逃生门”,让编译器根据概率选择一些大多数时候都能提升运行速度的优化手段,当激进优化的假设不成立时可通过逆优化(Deoptimization)退回到解释状态继续执行


故,在整个虚拟机执行架构中解释器与编译器经常配合工作


  • Xint 设置:用户可以使用参数 -Xint 强制虚拟机运行于 “解释模式”(Interpreted Mode),这时候编译器完全不介入工作。

  • -Xcomp 设置:强制虚拟机运行于 “编译模式”(Compiled Mode),这时候将优先采用编译方式执行,但是解释器仍然要在编译无法进行的情况下接入执行过程

  • -Xmixed 设置:这种配合使用的方式称为“混合模式”(Mixed Mode)


通过虚拟机 -version 命令可以查看当前默认的运行模式。

即时编译器(JIT 编译器)

JIT 编译器不是虚拟机的必需部分,但 JIT 编译器编译性能的好坏、代码优化程度的高低是衡量一款商用虚拟机优秀与否的最关键的指标之一,也是虚拟机中最核心且最能体现虚拟机技术水平的部分

被编译对象和触发条件

在运行过程中会被即时编译的“热点代码”有两类,即:

编译的目标对象

  • 被多次调用的方法

  • 编译器会将整个方法作为编译对象,这也是标准的 JIT 编译方式

  • 被多次执行的循环体

  • 由循环体出发的,但是编译器依然会以整个方法作为编译对象,因为发生在方法执行过程中,称为栈上替换



判断热点代码

「判断一段代码是否是热点代码,是不是需要出发即时编译」这样的行为称为热点探测(Hot Spot Detection),探测算法有两种,分别为

基于采样的热点探测(Sample Based Hot Spot Detection)

虚拟机会周期的对各个线程栈顶进行检查,如果某些方法经常出现在栈顶,这个方法就是“热点方法”


  • 优点:实现简单、高效,很容易获取方法调用关系。

  • 缺点:很难确认方法的 reduce(衰减),容易受到线程阻塞或其他外因扰乱



基于计数器的热点探测(Counter Based Hot Spot Detection)

为每个方法(甚至是代码块)建立计数器,执行次数超过阈值就认为是“热点方法”


  • 优点:统计结果精确严谨。

  • 缺点:实现麻烦,不能直接获取方法的调用关系


HotSpot 使用的是第二种-基于技术其的热点探测,并且有两类计数器:


  • 方法调用计数器(Invocation Counter )

  • 回边计数器(Back Edge Counter )



两个即时编译器

从上面的解释器和编译器的协同合作架构图中,应该可以了解到,JVM 虚拟机实现了两个不同的 JIT 编译器,分别称为 Client Compiler 和 Server Compiler ,或者简称为 C1 编译器和 C2 编译器

热点触发的阈值

这两个计数器都有一个确定的阈值,超过后便会触发 JIT 编译,具体细节和内容下面会详细讲述。


上面提到了一下两种热点探测的计数器:

方法调用计数器(Invocation Counter )
  • 首先是方法调用计数器:

  • Client 模式下默认阈值是 1500 次。

  • Server 模式下是 10000 次。

  • 这个阈值可以通过 -XX:CompileThreshold 来人为设定。

  • 如果不做任何设置,方法调用计数器统计的并不是方法被调用的绝对次数,而是一个相对的执行频率,即一段时间之内的方法被调用的次数。(可以理解为滑动窗口)。

  • 当超过一定的时间限度,如果方法的调用次数仍然不足以让它提交给即时编译器编译,那么这个方法的调用计数器就会被减少一半,这个过程称为方法调用计数器热度的衰减(Counter Decay),而这段时间就成为此方法的统计的半衰期( Counter Half Life Time)。

  • 进行热度衰减的动作是在虚拟机进行垃圾收集时顺便进行的,可以使用虚拟机参数 -XX:CounterHalfLifeTime 参数设置半衰周期的时间 (时间窗口秒),单位是秒。整个 JIT 编译的交互过程如下图。




回边计数器(Back Edge Counter )
  • 作用是统计一个方法中循环体代码执行的次数,在字节码中遇到控制流向后跳转的指令称为“回边”( Back Edge )。

  • 显然,建立回边计数器统计的目的就是为了触发 OSR 编译。关于这个计数器的阈值, HotSpot 提供了 -XX:BackEdgeThreshold 供用户设置。


但是当前的虚拟机实际上使用了 -XX:OnStackReplacePercentage 来简介调整阈值,计算公式如下:



  • Client 模式, 公式为方法调用计数器阈值(CompileThreshold)X OSR 比率(OnStackReplacePercentage)/100 。其中 OSR 比率默认为 933,那么,回边计数器的阈值为 13995

  • Server 模式,公式为方法调用计数器阈值(Compile Threashold)X (OSR (OnStackReplacePercentage)- 解释器监控比率 (InterpreterProfilePercent))/100


其中 onStackReplacePercentage 默认值为 140,InterpreterProfilePercentage 默认值为 33,如果都取默认值,那么 Server 模式虚拟机回边计数器阈值为 10700 。

编译过程

默认情况下,无论是方法调用产生的即时编译请求,还是 OSR 请求,虚拟机在代码编译器还未完成之前,都仍然将按照解释方式继续执行,而编译动作则在后台的编译线程中进行。


用户可以通过参数 -XX:-BackgroundCompilation来禁止后台编译,这样,一旦达到 JIT 的编译条件,执行线程向虚拟机提交便已请求之后便会一直等待,直到编译过程完成后再开始执行编译器输出的本地代码。

虚拟机运行模式

目前的 HotSpot 编译器默认的是解释器和其中一个即时编译器配合的方式工作,具体是哪一个编译器,取决于虚拟机运行的模式,HotSpot 虚拟机会根据自身版本与计算机的硬件性能自动选择运行模式,用户也可以使用 -client 和 -server 参数强制指定虚拟机运行在 Client 模式或者 Server 模式。

Client Compiler(了解即可) :


它是一个简单快速的三段式编译器,主要关注点在于局部的优化,放弃了许多耗时较长的全局优化手段。


  • 第一阶段,一个平台独立的前端将字节码构造成一种高级中间代码表示(High-Level Intermediate Representaion , HIR)。在此之前,编译器会在字节码上完成一部分基础优化,如 方法内联,常量传播等优化。

  • 第二阶段,一个平台相关的后端从 HIR 中产生低级中间代码表示(Low-Level Intermediate Representation ,LIR),而在此之前会在 HIR 上完成另外一些优化,如空值检查消除,范围检查消除等,让 HIR 更为高效。

  • 第三阶段,在平台相关的后端使用线性扫描算法(Linear Scan Register Allocation)在 LIR 上分配寄存器,做窥孔(Peephole)优化,然后产生机器码。

Server Compiler(了解即可):

专门面向服务端典型应用并为服务端性能配置特别调整过的编译器也是一个充分优化过的高级编译器,几乎能达到 GNU C++编译器使用-02 参数时的优化强度会执行所有经典的优化动作。


  • 无用代码消除(Dead Code Elimination)、

  • 循环展开(LoopcUnrolling)、

  • 循环表达式外提(Loop Expression 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)上的大寄存器集合


编译速度远超传统静态优化编译器,相对 Client Compiler 代码质量有所提高,可减少本地代码执行时间,从而抵消额外的编译时间开销


如何从外部观察即时编译器的编译过程和编译结果?


  • -XX:+PrintCompilation 在即时编译时,打印被编译成本地代码的方法名称

  • -XX:+PrintInlining 在即时编译时,输出方法内联信息

  • -XX:+PrintAssembly 在即时编译时,打印被编译方法的汇编代码,虚拟机需安装反汇编适配器 HSDIS 插件,Product 版虚拟机需加入参数-XX:+UnlockDiagnosticVMOptions 打开虚拟机诊断模式

  • -XX:+PrintOptoAssembly 用于 Server VM,输出比较接近最终结果的中间代码表示,不需 HSDIS 插件支持

  • -XX:+PrintLIR 用于 Client VM,输出比较接近最终结果的中间代码表示,不需 HSDIS 插件支持

  • -XX:+PrintCFGToFile 用于 Client Compiler,将编译过程中各阶段数据(如,字节码、HIR 生成、LIR 生成、寄存器分配过程、本地代码生成等)输出到文件中

  • -XX:PrintIdealGraphFile 用于 Server Compiler,将编译过程中各阶段数据(如,字节码、HIR 生成、LIR 生成、寄存器分配过程、本地代码生成等)输出到文件中


注,要输出 CFG 或 IdealGraph 文件,需 Debug 或 FastDebug 版虚拟机支持,Product 版的虚拟机无法输出这些文件

发布于: 2021 年 06 月 01 日阅读数: 334
用户头像

我们始于迷惘,终于更高水平的迷惘。 2020.03.25 加入

🏆 【酷爱计算机技术、醉心开发编程、喜爱健身运动、热衷悬疑推理的”极客狂人“】 🏅 【Java技术领域,MySQL技术领域,APM全链路追踪技术及微服务、分布式方向的技术体系等】 🤝未来我们希望可以共同进步🤝

评论

发布
暂无评论
☕️【Java技术之旅】深入学习JIT编译器实现机制(原理篇)