java源程序 -----> java字节码文件 -------> 目标操作系统可以直接执行的语句
第一个过程叫前编译,第二个过程叫后编译(或者叫解释,jvm每从字节码文件获得一条指令后,会将其解释成目的操作系统能执行的语句)
为了加快程序执行速度,第二个过程中会有一些骚操作,比如:JIT即时编译,AOT提前编译等等
HotSpot的编译器
1 Client Compiler
HotSpot VM中带有一个Client Compiler C1编译器,它主要做以下事情:
- 局部简单可靠的优化,比如字节码上进行的一些基础优化,方法内联、常量传播等,放弃许多耗时较长的全局优化。
- 将字节码构造成高级中间表示(High-level Intermediate Representation,以下称为HIR),HIR与平台无关,通常采用图结构,更适合JVM对程序进行优化。
- 最后将HIR转换成低级中间表示(Low-level Intermediate Representation,以下称为LIR),在LIR的基础上会进行寄存器分配、窥孔优化(局部的优化方式,编译器在一个基本块或者多个基本块中,针对已经生成的代码,结合CPU自己指令的特点,通过一些认为可能带来性能提升的转换规则或者通过整体的分析,进行指令转换,来提升代码性能)等操作,最终生成机器码。
1.方法内联
对于Java类面向对象的语言,通常要调用多个方法来完成功能。执行时,要经历多次参数传递、返回值传递及跳转等,于是C1采取了方法内联的方式,即把调用到的方法的指令直接植入当前方法中。
例如一段这样的代码:
public void bar (){
….
bar2 ();
…
private void bar2 (){
// bar2
}
当编译时,如bar2代码编译后的字节数小于等于35字节,那么,会演变成类似这样的结构:
public void bar (){
…
// bar2
}
2.去虚拟化
去虚拟化是指在装载class文件后,进行类层次的分析,如发现类中的方法只提供一个实现类,那么对于调用了此方法的代码,也可进行方法内联,从而提升执行的性能。
public interface IFoo{
public void bar () ;
}
public class Foo implements IFoo {
public void bar () {
// Foo bar method
}
}
public class Demo{
public void execute (IFoo foo){
foo. bar();
}
}
2 Server Compiler
Server Compiler主要关注一些编译耗时较长的全局优化,甚至会还会根据程序运行的信息进行一些不可靠的激进优化。这种编译器的启动时间长,适用于长时间运行的后台程序,它的性能通常比Client Compiler高30%以上。目前,Hotspot虚拟机中使用的Server Compiler有两种:C2和Graal。
Server compiler又称为C2,较为重量级,C2采用了大量的传统编译优化技巧来进行优化,占用内存相对会多一些,适合于服务器端的应用。和C1不同的主要是寄存器分配策略及优化的范围,寄存器分配策略上C2采用的为传统的图着色寄存器分配算法;由于C2会收集程序的运行信息,因此其优化的范围更多在于全局的优化,而不仅仅是一个方法块的优化。收集的信息主要有:分支的跳转/不跳转的频率、某条指令上出现过的类型、是否出现过空值、是否出现过异常。
逃逸分析
逃逸分析的基本行为就是分析对象动态作用域:当一个对象在方法中被定义后,它可能被外部方法所引用,例如作为调用参数传递到其他地方中,称为方法逃逸。
public static StringBuffer craeteStringBuffer(String s1, String s2) {
StringBuffer sb = new StringBuffer();
sb.append(s1);
sb.append(s2);
return sb;
}
StringBuffer sb是一个方法内部变量,上述代码中直接将sb返回,这样这个StringBuffer有可能被其他方法所改变,这样它的作用域就不只是在方法内部,虽然它是一个局部变量,称其逃逸到了方法外部。甚至还有可能被外部线程访问到,譬如赋值给类变量或可以在其他线程中访问的实例变量,称为线程逃逸。
随着JIT编译器的发展,在编译期间,如果JIT经过逃逸分析,发现有些对象没有逃逸出方法,那么有可能堆内存分配会被优化成栈内存分配。但是这也并不是绝对的。就像我们前面看到的一样,在开启逃逸分析之后,也并不是所有User对象都没有在堆上分配。正是因为很多堆上分配被优化成了栈上分配,所以GC次数有了明显的减少。
基于逃逸分析,C2在编译时会做标量替换,栈上分配和同步削除
标量替换
同步削除
在动态编译同步块的时候,JIT编译器可以借助逃逸分析来判断同步块所使用的锁对象是否只能够被一个线程访问而没有被发布到其他线程。
如果同步块所使用的锁对象通过这种分析被证实只能够被一个线程访问,那么JIT编译器在编译这个同步块的时候就会取消对这部分代码的同步。这个取消同步的过程就叫同步削除,也叫锁消除。
JIT即时编译
即时编译器编译代码需要时间,一般编译出优化程度更高的代码(影响程序启动响应速度,但是会提高运行效率),编译会花费更多的时间。为了在程序启动响应速度和运行效率之间达到平衡,HotSpot虚拟机采用分层编译,分层编译的思想将JVM的执行状态划分为5层:
- 第1层:解释执行
- 第2层:执行不带Profiling(收集反应执行状态的数据)的C1本地代码
- 第3层:执行带方法调用次数和回边调用次数Profiling的C1本地代码
- 第4层:执行所有Profiling的C1本地代码
- 第5层:执行C2本地代码
在程序运行过程中,热点代码会触发编译,热点代码有以下两类:
- 被多次调用的方法
- 被多次执行的循环体
热点代码通过热点探测的方式进行判定,判定的方式大约有两种:
- 基于采样的热点探测:虚拟机周期性检查线程的栈顶,如果某个方法经常出现在栈顶,那么该方法即为热点方法。这种实现方式简单高效,但是精确度不够,一些阻塞的方法会被误判为热点方法。
- 基于计数器的热点探测:对每个方法(甚至方法块)建立计数器,执行次数超过一定的阀值就被判定为热点方法。
Hotspot虚拟机采用的是基于计数器的热点探测,虚拟机为每个方法准备了两类计数器:
- 方法调用计数器
- 回边计数器
在分层编译开启的情况下,触发编译由以下条件来判断:
- 方法调用次数大于由参数-XX:TierXInvocationThreshold指定的阀值乘以系数
- 方法调用次数大于由参数-XX:TierXMINInvocationThreshold指定的阀值乘以系数,并且方法调用次数和循环回边次数之和大于由参数-XX:TierXCompileThreshold指定的阀值乘以系数时
以上两个条件满足其中一个即可触发即时编译,系数会由虚拟机根据当前编译的方法数以及编译线程数动态调整。
AOT提前编译
提前编译是相对于即时编译的概念,提前编译能带来的最大好处是Java虚拟机加载这些已经预编 译成二进制库之后就能够直接调用,而无须再等待即时编译器在运行时将其编译成二进制机器码。理 论上,提前编译可以减少即时编译带来的预热时间,减少Java应用长期给人带来的“第一次运行慢”的 不良体验,可以放心地进行很多全程序的分析行为,可以使用时间压力更大的优化措施[1]。 但是提前编译的坏处也很明显,它破坏了Java“一次编写,到处运行”的承诺,必须为每个不同的 硬件、操作系统去编译对应的发行包;也显著降低了Java链接过程的动态性,必须要求加载的代码在 编译期就是全部已知的,而不能在运行期才确定,否则就只能舍弃掉已经提前编译好的版本,退回到 原来的即时编译执行状态。
指令重排序
重排序通常是编译器或运行时环境为了优化程序性能而采取的对指令进行重新排序执行的一种手段。重排序分为两类:编译期重排序和运行期重排序,分别对应编译时和运行时环境。
编译期重排序的典型就是通过调整指令顺序,在不改变程序语义的前提下,尽可能减少寄存器的读取、存储次数,充分复用寄存器的存储值。
happens-before(先行先发生原则)
先行发生原则(Happens-Before)是判断数据是否存在竞争、线程是否安全的主要依据。
先行发生是Java内存,模型中定义的两项操作之间的偏序关系,如果操作A先行发生于操作B,那么操作A产生的影响能够被操作B观察到。
Java内存模型中存在的天然的先行发生关系:
1. 程序次序规则:同一个线程内,按照代码出现的顺序,前面的代码先行于后面的代码,准确的说是控制流顺序,因为要考虑到分支和循环结构。
2. 管程锁定规则:一个unlock操作先行发生于后面(时间上)对同一个锁的lock操作。
- volatile变量规则:对一个volatile变量的写操作先行发生于后面(时间上)对这个变量的读操作。
4. 线程启动规则:Thread的start( )方法先行发生于这个线程的每一个操作。
5. 线程终止规则:线程的所有操作都先行于此线程的终止检测。可以通过Thread.join( )方法结束、Thread.isAlive( )的返回值等手段检测线程的终止。
6. 线程中断规则:对线程interrupt( )方法的调用先行发生于被中断线程的代码检测到中断事件的发生,可以通过Thread.interrupt( )方法检测线程是否中断
7. 对象终结规则:一个对象的初始化完成先行于发生它的finalize()方法的开始。
8. 传递性:如果操作A先行于操作B,操作B先行于操作C,那么操作A先行于操作C。
总结:一个操作“时间上的先发生”不代表这个操作先行发生;一个操作先行发生也不代表这个操作在时间上是先发生的(重排序的出现)。
时间上的先后顺序对先行发生没有太大的关系,所以衡量并发安全问题的时候不要受到时间顺序的影响,一切以先行发生原则为准。
注:如果A happens- before B,JMM并不要求A一定要在B之前执行。JMM仅仅要求前一个操作(执行的结果)对后一个操作可见,且前一个操作按顺序排在第二个操作之前。这里操作A的执行结果不需要对操作B可见;而且重排序操作A和操作B后的执行结果,与操作A和操作B按happens- before顺序执行的结果一致。在这种情况下,JMM会认为这种重排序并不非法(not illegal),JMM允许这种重排序。