运行期
Java虚拟机之运行期及其优化
在前面对编译期的介绍中我们提到了运行期,实际上运行期也是编译期的一种,是将字节码转化为机器代码的过程,但我们一般把前端编译期称为编译期,因此为了区分,将这一过程叫做运行期。
1、解释执行和编译执行
执行字节码的两种选择:
-
解释执行:将字节码一行一行解释成指令流去执行,执行指令前不耗时间且不生成中间本地代码但执行效率低
-
编译执行:将字节码以方法为单位解释成本地机器码后再执行,执行指令前会消耗时间且生成了中间本地带但执行效率高
部分商用虚拟机中,Java程序最初是通过解释器进行解释执行的,而根据“二八原则”——程序20%的代码占用了80%的资源,运行频繁的方法或代码段会被定为“热点代码”,为了提高执行效率,热点代码会被编译成本地机器码并进行各层次的优化。
2、HotSpot内的即时编译器
HotSpot有两个即时编译器C1(Client Complier)和C2(Server Complier),前者进行一些简单而可靠的优化,后者进行一些激进充分而不可靠的优化。
使用解释器时能迅速启动并执行程序,使用即时编译器能提高热点代码执行效率,而逆优化返回解释器或C1可以作为C2激进优化的逃生门。
- 相关虚拟机参数:
-Xint:解释模式——只使用解释器
-Xcomp:编译模式——只是用编译器
默认:混合模式——同时使用解释器和编译器
3、HotSpot的编译策略
为了实现编译本身的是将开销、编译过程中解释器收集监控信息开销与编译带来的收益之间的平衡,HotSpot使用分层编译策略
-
第一层编译:使用解释器执行,不使用编译器,那么也不需监控收集信息
-
第二层编译:开始使用编译执行进行简单的优化,同时需要监控收集信息
-
第三层编译:使用编译执行进行简单优化的同时还进行对一些执行频率更高的热点代码进行一些激进的优化
4、编译对象和触发条件
编译针对高频出现的方法和循环体,而且由于循环体被包含在方法中,编译循环体时会把整个方法进行编译,因此可以说编译针对方法进行优化。另外,编译是在方法被执行的过程中发生并最终将栈上方法的指令替换成优化后的指令,因此这种编译方式也成为栈上替换。
-
触发编译的条件时某一个方法被定为热点代码,而热点代码的判定有两种方法:基于采样的热点探测和基于计数器的热点探测
-
基于采样的热点探测:周期性检查各个栈的栈顶,某些方法经常出现在栈顶,则可以判定该方法就是热点方法。实现简单还能容易获取方法调用关系,但很容易收线程阻塞或别的外界因素影响导致探测不准确。
-
基于计数器的热点探测:为每个方法设置计数器,统计每个方法的执行次数,当执行次数超过阙值则认定该方法为热点方法。实现比较麻烦且无法获取方法调用关系,但探测结果准确。
-
相关虚拟机参数:-XX:+PrintCOmpilation:打印出虚拟机在即时编译时被编译的本地机器代码的方法名称
HotSpot使用基于计数器的热点探测方法,为方法设置了方法计数器,为循环代码段设置了回边计数器。
-
方法计数器:记录某段时间内方法的出现频率,如果超过这段时间后方法还没到达热点阙值,那么虚拟机会将该计数器的技术结果减半,这个过程称为热度衰减,该段时间称为半衰周期。
- 相关的虚拟机参数:
-XX:CompileThreshold——热点阙值
-XX:-UseCounterDecay——热度衰减开关
-XX:CounterHalfLifeTime——设置半衰周期,时间单位是秒
- 相关的虚拟机参数:
-
回边计数器:回边指的是字节码中遇到控制流向后跳转的指令,回边计数器不存在热度衰减,当计数器溢出时会将方法计数器的值也调到溢出状态。而回边阙值需要根据参数-XX:OnStackReplacePercentage来间接调整,计算公式如下:
-
在Client模式下:方法计数器阙值乘以OSR比率再除以100
-
在Server模式下:方法计数器阙值乘以(OSR比率减去监视器监控比率的差值)再除以100
-
-
5、编译过程
-
Client编译器:字节码——>高级中间代码表示[HIR(静态单分配形式表示代码值——SSA)]——>低级中间代码表示(LIR)———窥孔优化—> 本地机器代码
-
Server编译器:充分优化的高级编译器,会执行所有经典优化并进行一些全局激进优化
6、优化技术
虚拟机有很多优化技术,我们只列举一些典型的优化例子。
-
I.公共子表达式消除:若某一表达式在当前位置已被计算,那么在其他地方时,其中变量的值也未改变则无需再计算直接使用先前计算结果
-
II.代数化简:根据代数运算律化简表达式
-
III.复写传播:如果z=y,则在后续的指令中将z全部替换成y以配合其他优化方法
-
IV.无用代码消除:永远不会被执行或者完全没有意义的代码将被消除
-
V.数组边界检查消除:在编译期提前判断数组下标是否超界以省去运行期判断成本
-
VI.方法内联:把被调用的方法的代码“复制”到发起调用的方法的代码中
-
困难:编译期虚方法不进行编译,导致无法直接在编译期进行内联,不编译的原因在于一旦方法出现重写或重载,方法需要进行动态链接
-
方案:引入类型继承关系分析(CHA:用来判断方法接收者是否是该方法的合理接收者)和内联缓存(每个方法入口设置一个内联缓存池),若CHA查询结果有多个则使用内联方法:第一次内联后缓存方法接收者版本信息,当出现方法接收者改变时取消内联,否则该内联可以一直使用。也就是说一个虚方法只有一次内联机会,而一旦出现多个方法接收者,内敛将会被取消。
-
-
VII.逃逸分析:分析对象动态作用域,如果不被外部方法或外部线程引用,则该对象不可逃逸,不可逃逸的对象是安全的。针对不可逃逸对象可以进行的优化有:
-
①栈上分配:将该对象数据分配到栈上(局部变量表中),随方法结束而自动销毁,这样可以大幅减少垃圾收集器的压力,也节省了堆内存
-
②同步消除:不被外部线程引用,则可以不设置同步措施,即不用设置读写锁等。
-
③标量替换:将对象分解成不可再分解的数据,继而可以配合诸如栈上分配等的优化手段。
-
7、Java编译期和C/C++编译器对比
-
劣势:
-
I.编译占用用户运行时间
-
II.需要检查语义
-
III.多态选择频率高
-
IV.扩展加载新的类将导致全局优化难以进行
-
V.对象分配在堆内存,GC成本高
-
-
优势:
-
I.造成劣势的方法却提高了代发效率
-
II.可监控性能
-