运行期优化
本章讲述针对《008.编译期优化.md》中第二类编译过程的优化。
1. 概述
Java程序最初是通过解释器进行解释执行的,当虚拟机发现某个方法、代码块的运行特别频繁时,就会把这些代码认定为"热点代码"。为了提高热点代码的执行效率,在运行时,虚拟机将会把这些代码编译成与本地平台相关的机器码,并进行各种层次的优化,完成这个任务的编译器称为即时编译器(JIT、Just In Time)。
2. HotSpot的即时编译器
2.1 解释器与编译器
解释器可以作为编译器激进优化的一个逃生门,让编译器根据概率选择一些大多数时候都能提升运行速度的优化手段,当激进优化的假设不成立,如加载了新类后类型继承结构出现变化、出现"罕见陷阱"时可以通过逆优化退回到解释状态继续执行。
HotSpot内置了两个即时编译器:Client Compiler(C1),Server Compiler(C2)。
解释器、编译器搭配模式:
-
混合模式(mixed mode)
解释器、编译器搭配使用。-client使用C1编译器,-server使用C2编译器。
-
解释模式(interpreted mode)
只有解释器工作,使用-Xint指定。
-
编译模式(compiled mode)
优先采用编译方式执行程序,但是解释器仍要在编译无法进行情况下介入执行过程。使用-Xcomp指定。
解释器与编译器共同使用时,想要编译出优化程度更高的代码,解释器可能还要替编译器收集性能监控信息,这对解释器执行速度有影响。为了达到平衡,采用分层编译:
- 第0层,程序解释执行,解释器不开启性能监控工具(Profiling),可触发第1层编译。
- 第1层,也称为C1编译,将字节码编译为本地代码,进行简单、可靠的优化,如有必要将加入性能监控的逻辑。
- 第2层,也称为C2编译,将字节码编译为本地代码,但是会启用一些编译耗时较长的优化,甚至会根据性能监控信息进行一些不可靠的激进优化。
实施分层编译后,Client Compiler、Server Compiler将会同时工作,许多代码都可能会被多次编译,用Client Compiler获取更高的编译速度,用Server Compiler获取更好的编译质量,在解释执行时也无需再承担收集性能监控信息的任务。
2.2 编译对象与触发条件
热点代码分为两类:
-
被多次调用的方法
编译器会以整个方法为编译对象,是虚拟机中标准的JIT编译方式。
-
被多次执行的循环体
尽管编译动作是循环体触发,但编译器仍以整个方法(而不是单独的循环体)作为编译对象。这种编译方式因为编译发生在方法执行过程中,因此称为栈上替换(OSR编译,即方法栈帧还在栈上,方法被替换了。)
判断一段代码是不是热点代码,是不是需要触发即时编译,这样的行为称为热点探测。热点探测方式有两种:
-
基于采样的热点探测
周期性地检查各个线程的栈顶,如果发现某个方法经常出现在栈顶,就是热点方法。
容易受到线程阻塞或别的外界因素影响而扰乱热点探测。
-
基于计数器的热点探测
为每个方法建立计数器,统计方法的执行次数。如果执行次数超过一定的阈值就认定为热点方法。
HotSpot采用这种方式。
方法调用计数器、回边计数器(统计方法中循环体代码执行的次数)
如果不做任何设置,方法调用计数器只是统计一段时间内的次数,当超过时间后,会进行热度衰减。
3. 编译优化技术
只介绍几种编译优化技术
3.1 公共子表达式消除
如果一个表达式E已经计算过了,并且从先前的计算到现在E中所有变量的值都没有发生变化,那么E的这次出现就称为了公共子表达式。没有必要花时间再对它进行计算,只需要直接用前面计算过的表达式结果代替E就可以了。
3.2 数组边界检查消除
3.3 方法内联
方法内联是指把目标方法的代码复制到发起调用的方法中,避免发生真实的方法调用。
3.4 逃逸分析
逃逸分析的基本行为是分析对象动态作用域:当一个对象在方法中被定义后,它可能被外部方法所引用,例如作为调用参数传递到其他方法中,称为方法逃逸。甚至还有可能被外部线程访问到,例如赋值给类变量或可以在其他线程中访问的实例变量,称为线程逃逸。
如果能证明一个对象不会逃逸到方法或线程之外,也就是别的方法或线程无法通过任何途径访问到这个对象,则可能为这个变量进行一些高效的优化,如:
-
栈上分配
如果确定一个对象不会逃逸出方法之外,那让这个对象在栈上分配内存将会是一个很不错的主意,对象所占用的内存空间就可以随栈帧出栈而销毁。
-
同步消除
如果逃逸分析确定一个变量不会逃逸出线程,无法被其他线程访问,那这个变量的读写就不会有竞争,对这个变量实施的同步措施就可以消除掉。
-
标量替换
标量是指一个数据已经无法再分解为更小的数据来表示了。
Java虚拟机中的原始数据类型(int、long等)都不能再进一步分解,他们可以称为标量。
如果一个数据可以继续分解,它就称为聚合量。Java中的对象就是最典型的聚合量。
如果把一个Java对象拆散,根据程序访问的情况,将其使用到的成员变量恢复原始类型来访问就叫做标量替换。
如果逃逸分析证明一个对象不会被外部访问,并且这个对象可以被拆散的话,那程序真正执行时,可能不创建这个对象,而改为直接创建它的若干个被这个方法使用到的成员变量来代替。
将对象拆分后,除了可以让对象的成员变量在栈上分配和读写外,还可以为后续进一步优化手段创造条件。