后端编译:把 Class 文件转换成与本地基础设施(硬件指令集、 操作系统) 相关的二进制机器码的过程。

# 即时编译器

即时编译器

  • 热点代码:在 java 程序通过解释器进行解释执行时,当某个方法或代码块运行特别频繁就认定这些为热点代码。
  • 即时编译器:完成虚拟机将热点代码编译成本地机器码并优化的过程。

# 解释器与编译器

解释器与编译器各自优势

  • 解释器:当程序需要迅速启动和执行时,使用解释器省区编译时间立即运行。
  • 编译器:程序启动后编译器逐步将更多的代码编译成本地代码,减少解释器中间损耗,提高执行效率。

解释器编译器交互

HotSpot虚拟机内置即时编译器
  • C1 编译器:客户端编译器
  • C2 编译器:服务端编译器

JDK7 中默认开启分层编译策略

  • 第 0 层:程序仅解释执行并且不开启性能监控。
  • 第 1 层:使用 C1 编译器将字节码编译为本地代码进行优化。不开启性能监控。
  • 第 2 层:使用 C1 编译器执行,仅开启方法及回边次数统计等有限性能监控功能。
  • 第 3 层:使用 C1 编译器执行,开启全部性能监控。
  • 第 4 层:使用 C2 编译器执行(启用更多编译耗时更长的优化),并根据性能监控进行不可靠的激进优化。
  • 分层编译模型

# 编译对象(热点代码)与触发条件

热点代码包括:

  • 被多次调用的方法。
  • 被多次执行的循环体所在的方法(因为编译发生在方法执行过程中:栈上替换)。

热点探测:热点代码判定方法

  • 基于采样的热点探测
  • 实现:虚拟机周期性地检查各个线程调用栈顶,当某个方法经常出现在栈顶,即为 “热点方法”。
  • 优点:实现简单高效,很容易地获取方法调用关系(展开堆栈即可)。
  • 缺点:很难精确确认方法热度,受到线程阻塞或别的外界因素影响。
  • 基于计数器的热点探测
  • 实现:为每个方法(代码块)建立计数器统计执行次数(相对频度),如果超过一定阈值即为 “热点方法”。
  • 优点:统计结果更加精确严谨。
  • 缺点:较为繁琐,需要为每个方法建立并维护计数器,不能直接获取方法调用关系。
  • 调用计数器热度衰减:当方法在一定时间限度调用次数仍不足以提交给即时编译器编译,那么该方法的调用计数器会被减少一半。该动作伴随垃圾收集时进行,可使用虚拟机参数 - XX: -UseCounterDecay 关闭热度衰减。
  • 半衰周期:进行热度衰减的时间周期。使用 - XX:CounterHalfLifeTime 参数(秒)设置。
方法调用计数器触发即时编译

方法调用计数器即时编译

回边计数器

  • 统计一个方法中循环体代码执行次数。
  • 回边:在字节码中遇到控制流向后跳转的指令。
  • 目的是为了触发栈上的替换编译。
回边计数器触发即时编译

回边计数器触发即时编译

# 编译过程

默认条件下(-XX:BackgroundCompilation 参数开启时)收到编译请求时,虚拟机在按照解释执行方式继续执行代码的同时,在后台的编译线程中进行。当禁止后台编译(参数关闭),达到触发即时编译条件时,执行线程将一直阻塞等待编译完成。

客户端编译器(简单快速的三段式编译器)

  • 第一阶段:一个平台独立的前端将字节码构造成一种高级中间代码(HIR:与目标机器指令集无关的中间表示)表示。
  • 编译器在字节码上完成部分基础优化(方法内联、常量传播等)会在被构造成 HIR 之前完成。
  • 第二阶段:一个平台相关的后端从 HIR 中产生低级中间代码表示(LIR:与目标机器指令集相关的中间表示)。
  • 编译器在 HIR 上完成另外优化(空值检查消除、范围检查消除等)会在构造 LIR 之前完成。
  • 第三阶段:在平台相关的后端使用线性扫描算法(LSRA)在 LIR 上分配寄存器,并做窥孔优化后产生机器代码。
C1编译器架构

C1编译器架构

服务端编译器

  • 采用全局图着色的寄存器分配器。
  • 相对于客户端编译器输出的代码质量有很大提高。

# 提前编译器

提前编译的两条明显分支

  • 传统的应用形式:在程序运行之前把程序代码编译成机器码的静态翻译工作。
  • 解决即时编译的最大弱点:对程序运行时间和运算资源的占用。
  • 对即时编译器做缓存加速(动态提前编译或即时编译缓存):把原本的即时编译器在运行时要做的编译工作提起做好并保存。
  • 改善 java 程序的启动时间,以及需要一段时间预热才能达到最高性能的问题。
即时编译器相对于提前编译器的优势
  • 性能分析制导优化:条件分支热点路径优化
  • 激进预测性优化:虚方法内联
  • 链接时优化:跨越动态链接库的方法内联

# hotspot 虚拟机编译器优化技术

方法内联
  • 概念:把目标方法原封不动地 “复制” 到发起调用地方法之中,避免发生真实的方法调用。
  • 优化:能够消除方法调用成本,并为其他优化手段建立良好基础。
  • 非虚方法:直接进行内联
  • 解决虚方法内联问题:
  • 类型继承关系分析(CHA):确定在目前已加载的类中,某个接口是否多余一种实现、某个类是否存在子类、某个子类是否覆盖父类的某个虚方法等。
  • 守护内联(激进预测性优化):如果 CHA 分析后方法只有一个版本,就可以假设 “应哟个的全貌就是当前状态” 则可以进行内联。但必须预留当假设不成立时的 “退路”。
  • 内联缓存:如果 CHA 查询结果方法由多个版本供选择,GIT 将使用内联缓存缩减方法调用开销(仍是真正的方法调用,但避免了查虚方法表的开销)。
  • 未发生方法调用之前,内联缓存为空。
  • 第一次调用发生后,缓存记录下方法接收者的版本信息
  • 每次进行方法调用都比较接收者版本。
  • 单态内联缓存:之后的每次调用,方法接收者版本都一样。
  • 比用不内联的非虚方法调用仅多一次类型判断开销。
  • 超多态内联缓存:方法接收者不一致。
  • 相当于真正查找虚方法表进行方法分派的开销。
逃逸分析
  • 原理:分析对象动态作用域根据逃逸程度做不同的优化。
  • 方法逃逸:当一个对象在方法里被定义后,被外部方法引用的(例:作为调用参数传递到其他方法)。
  • 线程逃逸:当一个对象在方法里被定义后,被其他线程引用(例:赋值给可以在其他线程中访问的实例变量)。
  • 如果能证明一个对象不会逃逸到方法或线程之外,或只逃逸出方法不会逃逸出线程,则可能为这个对象实例采取不同的优化:
  • 栈上分配:如果对象不会逃逸出线程之外,可以让对象在栈上分配内存,其所占内存空间可以随栈帧出栈一起被销毁(节省了垃圾收集和回收占用的资源)。
  • 标量替换:对于不会逃逸出方法范围的对象,将该对象拆散,根据程序访问情况将其用到的成员变量恢复为原始数据类型访问。
  • 标量:无法再分解成更小的数据(如:java 虚拟机中的原始数据类型)。
  • 聚合量:可以继续分解的数据(如:java 对象)。
  • 同步消除:对于不会逃逸出线程的变量,可将其的同步措施安全地消除掉。
公共子表达式消除
  • 概念:如果表达式 E 之前已被计算过,并从先前的计算到现在 E 中所有变量的值没有发生变化,那么 E 的这次出现就称为公共子表达式,可以用之前计算过的表达式结果代替 E。
  • 局部公共子表达式消除:优化仅限于程序基本块。
  • 全局公共子表达式消除:优化范围涵盖多个基本块。
数组边界检查消除
  • 概念:如果编译器只通过数据流分析就可判定循环变量取值范围永远在合法范围内,那么循环中可以把整个数组的上下界检查消除掉。