晚期(运行期)优化

一、概述

热点代码:某个方法或代码块运行的特别频繁时,Hotspot 虚拟机就会将这些代码认定为热点代码。

然后会用 JIT 编译器将这些代码编译成机器码,并进行各种层次的优化。

二、Hotspot 内的即时编译器

1、解释器与编译器并存

解释器:可以省去编译时间,立即执行

编译器:执行效率更高

逆优化可以使代码退回到解释状态继续执行。

Client Compiler:C1

Server Compiler:C2

默认采用解释器与其中一个编译器直接配合的方式工作。

选用 C1 还是 C2 取决于虚拟机的运行模式,可以通过 -client 或者-server 参数来指定

Hotspot 虚拟机用分层编译策略:

  • 第 0 层,程序解释执行,解释器不开启性能监控功能,可触发第 1 层编译
  • 第 1 层,也称为 C1 编译,将字节码编译为本地代码,进行简单可靠的优化,有必要的话将加入性能监控的逻辑
  • 第 2 层,称为 C2 编译,也是将字节码编译为本地代码,但是会启用一些编译耗时较长的优化,甚至会根据性能监控信息来采取一些不可靠的激进优化。

分层编译的情况下,C1 和 C2 同时工作,许多代码会被多次编译,用 C1 获取更高的编译速度,用 C2 获取更好的编译质量。

2、编译对象与触发条件

热点代码:

  • 被多次调用的方法
  • 被多次执行的循环体

这两种都以整个方法作为编译对象,但是后者的编译发生在方法执行过程之中,所以被称为栈上替换 OSR 编译。

Hotspot 采用的是基于计数器的热点探测方法,为每个方法准备了 方法计数器和回边计数器

这两个计数器都有一个阈值,超过阈值后会触发 JIT 编译。

1)方法计数器触发 JIT 过程

方法计数器不是精确的绝对值,他指的是一段时间内方法被调用的次数。

当超过一段时间限度,计数器会被减半,称为热度衰减,在 GC 时发生。

2)回边计数器触发 JIT 过程

在字节码中遇到控制流向后跳转的指令称为回边。

3、编译过程

默认情况下,JIT 编译和 OSR 编译都是在后台线程进行的,在他们完成之前,虚拟机按照解释方式进行。

后台编译主要有三个阶段:


这是 Client 的三个阶段:

  1. 将字节码进行一部分优化,并构造成一种高级中间代码表示 HIR
  2. 将 HIR 进行一部分优化,并产生低级中间代码 LIR
  3. 在后端使用线性扫描算法在 LIR 上分配寄存器等,产生本地机器代码

Server Compiler 是面向服务端的,并为服务端性能配置调整优化过的编译器。

  • 会执行所有的经典优化动作
  • 寄存器分配器是一个全局图着色分配器
  • 编译速度超过传统的静态优化编译器,输出代码质量高于 client ,可以减少本地代码执行时间

三、编译优化技术

JIT 编译器有着所有的代码优化措施,因此 JIT 产生的本地代码会比 javac 产生的字节码更优秀。

1、公共子表达式消除

就是一个计算过的表达式,如果没有改变,就可以复用他的值,不需要重新计算

2、数组边界检查消除

每次数组元素的读写都带有一次隐含的条件判定操作,当频繁操作数组时,是一种性能负担。

编译器可以通过数据流分析就可以判定循环变量的取值范围永远在[0, foo.length)之内,那么在整个循环中就可以把数组的上下边界检查消除,可以节省很多次的条件判断。

3、方法内联

一是为了消除方法调用的成本,二是为了其他优化手段打基础。

但是,只有静态方法、私有方法、实例构造器、父类方法,这 4 类是非虚方法,是在编译期间就可以解析确定的。其他方法调用都可能存在多个选择。

所以虚拟机团队用了 CHA 技术,类型继承关系分析,用于确定在目前已加载的类中,某个接口是否有多于一种的实现,某个类是否存在子类等信息。

当遇到虚方法时,编译器会向 CHA 查询该方法的版本,如果只有一个版本,直接进行内联。

如果有多个版本,那么可以使用内联缓存,当调用方法接受者发生变化时,再取消内联,从虚方法表中进行方法分派。

4、逃逸分析

当一个方法中的对象传递到其他方法中时,称为方法逃逸,当被其他线程访问到时,称为线程逃逸。

如果可以证明一个对象不会逃逸,就可以针对他做一些优化:

  • 栈上分配:对不会逃逸的对象使用栈上分配,可以让他随着方法结束而自动销毁,减少了 GC 消耗
  • 同步消除:对不会逃逸的对象,可以省略同步的措施
  • 标量替换:可以将不会逃逸的对象拆分成一个个标量,然后分配在栈上

但是目前判断一个对象是否会逃逸,成本很高,所以虚拟机中都是默认关闭的。

四、Java 与 C/C++编译器对比

Java 编译器的劣势:

  • Java 的即时编译器会占用用户的运行时间,所以优化手段也受制于编译成本。而 C++的静态优编译器对时间不是主要关注
  • Java 是动态的类型安全语言,所以虚拟机要频繁的进行动态检查,这样会消耗不少时间
  • Java 调用虚方法的频率远远高于 C 语言,在对方法接受者多态选择的频率也更高,因此优化难度高于 C 的静态编译器
  • Java 是动态扩展的,因此编译器很难进行全局的优化,有时候只能采用激进方式
  • Java 对象分配都在堆上,而 C有多种内存分配方式,加上 C 用代码进行内存回收,因此运行效率要高于 GC,内存回收压力也更小