1、概述
Java程序最初是通过解释器(Interpreter)进行解释执行的,当虚拟机发现某个方法或代码块的运行特别频繁时,就会把这些代码认定为“热点代码”(Hot Spot Code)。
为了提高热点代码的执行效率,在运行时,虚拟机将会把这些代码编译成与本地平台相关的机器码,并进行各种层次的优化,完成这个任务的编译器称为即时编译器(Just In Time Compiler,下文中简称JIT编译器)。
2 HotSpot虚拟机内的即时编译器
2.1、解释器和编译器
解释器的优势:快速启动和执行,节约内存。
编译器的优势:更高的执行效率。
HotSpot虚拟机中内置了两个即时编译器,分别称为Client Compiler和Server Compiler,或者简称为C1编译器和C2编译器。
用户可以使用参数“-Xint”强制虚拟机运行于“解释模式”(Interpreted Mode) ,也可以使用参数“-Xcomp”强制虚拟机运行于“编译模式”(Compiled Mode)
2.2、分层编译
分层编译根据编译器编译、 优化的规模与耗时,划分出不同的编译层次,其中包括:
第0层,程序解释执行,解释器不开启性能监控功能(Profiling),可触发第1层编译。
第1层,也称为C1编译,将字节码编译为本地代码,进行简单、 可靠的优化,如有必要将加入性能监控的逻辑。
第2层(或2层以上),也称为C2编译,也是将字节码编译为本地代码,但是会启用一些编译耗时较长的优化,甚至会根据性能监控信息进行一些不可靠的激进优化。
解释器还可以作为编译器激进优化时的一个“逃生门”
2.3、编译对象与触发条件
在运行过程中会被即时编译器编译的“热点代码”有两类,即:
被多次调用的方法。
被多次执行的循环体 -- 仍然会编译整个方法
热点探测方法:
基于采样的热点探测(Sample Based Hot Spot Detection) :采用这种方法的虚拟机会周期性地检查各个线程的栈顶,如果发现某个(或某些)方法经常出现在栈顶,那这个方法就是“热点方法”。 容易受干扰。
基于计数器的热点探测(Counter Based Hot Spot Detection): 麻烦,但精确。
HotSpot虚拟机中使用的是第二种——基于计数器的热点探测方法 ,因此它为每个方法准备了两类计数器:方法调用计数器(Invocation Counter)和回边计数器(Back Edge Counter)。 当计数器超过阈值溢出了,就会触发JIT编译 。
方法调用计数器
当超过一定的时间限度,如果方法的调用次数仍然不足以让它提交给即时编译器编译,那这个方法的调用计数器就会被减少一半,
这个过程称为方法调用计数器热度的衰减(Counter Decay),
而这段时间就称为此方法统计的半衰周期(Counter Half Life Time)。
回边计数器:对代码段执行次数计数
它的作用是统计一个方法中循环体代码执行的次数[2],在字节码中遇到控制流向后跳转的指令称为“回边”(Back Edge)
回边计数器没有计数热度衰减的过程,因此这个计数器统计的就是该方法循环执行的绝对次数。
2.4、编译过程
这里就会用到很多优化手段,其中Client侧用的少些,Server侧用的多些。
优化方法后面讲,
还可以通过参数配置查看优化过程,但需要Debug版本的虚拟机。
3、编译优化技术
3.1 优化技术概览
3.2、方法内联(Method Inlining)
方法内联的重要性要高于其他优化措施,它的主要目的有两个,
一是去除方法调用的成本(如建立栈帧等),
二是为其他优化建立良好的基础,方法内联膨胀之后可以便于在更大范围上采取后续的优化手段,从而获取更好的优化效果。
因此,各种编译器一般都会把内联优化放在优化序列的最靠前位置。
方法内联的难点:虚方法。因为虚方法在编译时是无法确定指向的,而java又大量存在虚方法。
所以在许多情况下虚拟机进行的内联都是一种激进优化。
3.3、公共子表达式消除(Common Subexpression Elimination)
原理很简单,int d=(c * b)*12+a+(a+b * c);此表达式中,b*c即被公共子表达式。
3.4、数组边界检查消除(Array Bounds Checking Elimination)
解决问题:数组每次访问都去判断下标是否越界太耗性能。
思路:
1、把运行期检查提前到编译期完成,通过语义,断定不会异常的就不检查。
2、隐式异常处理,直接访问,然后捕获segment_fault,转化抛出该抛的异常。
3.5、逃逸分析(Escape Analysis)
并不是直接优化代码的手段,而是为其他优化手段提供依据的分析技术。
逃逸分析的基本行为就是分析对象动态作用域:
当一个对象在方法中被定义后,它可能被外部方法所引用,例如作为调用参数传递到其他方法中,称为方法逃逸。
甚至还有可能被外部线程访问到,譬如赋值给类变量或可以在其他线程中访问的实例变量,称为线程逃逸。
对未逃逸对象的优化方法:
1、栈上分配(Stack Allocation) ,可极大减小内存回收的压力。 -- HotSpot中暂时还没有做这项优化。
2、同步消除(Synchronization Elimination) :由于对象不会被其他线程使用,所以不需要同步锁了。
3、标量替换(Scalar Replacement) :把局部对象拆成多个局部标量,除了可以用栈存标量外,还可以做进一步优化。
标量(Scalar)是指一个数据已经无法再分解成更小的数据来表示了,Java虚拟机中的原始数据类型(int、 long等数值类型以及reference类型等)都不能再进一步分解,它们就可以称为标量。
相对的,如果一个数据可以继续分解,那它就称作聚合量(Aggregate),Java中的对象就是最典型的聚合量。
如果把一个Java对象拆散,根据程序访问的情况,将其使用到的成员变量恢复原始类型来访问就叫做标量替换。
逃逸分析的缺点:
不能保证逃逸分析的性能收益必定高于它的消耗。
如果要完全准确地判断一个对象是否会逃逸,需要进行数据流敏感的一系列复杂分析,从而确定程序各个分支执行时对此对象的影响。
这是一个相对高耗时的过程,如果分析完后发现没有几个不逃逸的对象,那这些运行期耗用的时间就白白浪费了,所以目前虚拟机只能采用不那么准确,但时间压力相对较小的算法来完成逃逸分析。
还有一点是,基于逃逸分析的一些优化手段,如上面提到的“栈上分配”,由于HotSpot虚拟机目前的实现方式导致栈上分配实现起来比较复杂,因此在HotSpot中暂时还没有做这项优化。
4、Java与C/C++的编译器对比
Java与C/C++的编译器对比实际上代表了最经典的即时编译器与静态编译器的对比。
JAVA的劣势:
1、即时编译占运行时间。
2、动态类型安全检查耗费时间。
3、虚方法的使用频率远远大于C++,多态选择的频率也就大。
4、动态扩展使得全局优化难以进行。
5、堆内存回收
Java的劣势虽多,但劣势都是为了换取开发效率上的优势而付出的代价。
Java的优势:
1、即时编译器可以在运行时根据动态状态做优化。如调用频率等。