将 “热点代码” 编译成本地机器码,并进行优化,完成此过程的编译器成为即时编译器。 某个方法或代码块运行特别频繁,这些代码就称为“热点代码”。
1. 解释器与编译器
许多主流的商用虚拟机同时包含解释器和编译器。 当程序需要迅速启动和执行的时候,解释器可以首先发挥作用,省去编译的时间,立即执行。 在程序运行后,随着时间的推移,编译器逐渐发挥作用,把越来越多的代码编译成本地代码之后,可以获取更高的执行效率。
解释器抽象表现
输入的代码 → [ 解释器 解释执行 ] → 执行结果
JIT 即时编译器抽象表现
输入的代码 → [ 编译器 编译 ] → 编译后的代码 → [ 执行 ] → 执行结果
HotSpot虚拟机中内置两个即时编译器,分别为Client Compiler(C1编译器)和Server Compiler(C2编译器)。 C1编译器和C2编译器都将字节码编译为本地代码,但C1只进行简单、可靠的优化,C2还会启用一些高级优化,甚至还会进行一些不可靠的激进优化。
2. 编译对象和触发条件
在运行过程中会被即时编译器编译的“热点代码”有两类,即:
- 被多次调用的方法
- 被多次执行的循环体
判断一段代码是不是热点代码,是不是需要触发即时编译,这样的行为称为热点探测。目前主流的热点探测有两种:
- 基于采样的热点探测
- 基于计数器的热点探测
HotSpot虚拟机采用基于计数器的热点探测方法。
3. 编译优化技术
1. 公共子表达式消除
- 公共子表达式 如果一个表达式E已计算过,且E中所有变量值没有改变,那么E的这次出现称为公共子表达式。
- 公共子表达式消除 直接使用表达式E结果代替E称为公共子表达式消除。
- 局部公共子表达式消除 优化仅限于程序的基本块内
- 全局公共子表达式消除 优化范围涵盖多个基本块
2. 数组边界检查消除
Java语言中访问数组数据时系统会自动进行上下界的范围检查。对于程序开发者,如果未编写防御代码,也能避免大量的溢出攻击。但对于虚拟机的执行子程序,如果存在大量数据访问的程序代码,这无疑是一种性能负担。
那么如何消除运行期数组的边界检查呢?我们可以在编译器根据数据流分析来确定数组length的值,并判断访问下标是否越界,执行时也就无须判断。也就是把运行期的数组边界检查提到编译期完成,从而消除运行期时这些隐式开销。
3. 方法内联
把目标方法的代码“复制”到发起调用的方法中,避免发生真实的方法调用。 (Java虚拟机中的方法内联没有这么简单,稍后讲解。自己标记下)
4. 逃逸分析
逃逸分析的基本行为就是分析对象动态作用域。
- 方法逃逸 当一个对象在方法中被定义后,它可能被外部方法所引用,例如作为调用参数传递到其他方法中。
- 线程逃逸 还有可能被外部线程访问到,譬如赋值给类变量或可以在其他线程中访问的实例变量
参考
- 《深入理解Java虚拟机》第二版
- 知乎 https://www.zhihu.com/question/36746487