执行引擎
概述
执行引擎顾名思义就是用于让程序执行起来的引擎。我们知道,我们编写的java源代码.java文件运行前需要进行编译生成对应的.class字节码文件,这里的编译我们可以称为“前端编译”,这样才能够被类加载器识别并加载到JVM内存中,但由于硬件是不认识字节码指令的,更不认识高级语言编写的源代码,因此就需要一个东西去把字节码指令翻译成机器码指令,这样才能够去运行,执行引擎就是这个翻译者。执行引擎通过PC寄存器的地址读取对应的字节码指令,然后解析或编译成机器码指令让程序运作起来。执行引擎可以通过解释器或者JIT(Just In Time)编译器,也就是及时编译器来实现,两者可以共存也可以独立存在。早期的官方java虚拟机的执行引擎就只有解释器,而现在的HotSpot虚拟机则是解释器和JIT编译器并存,JRockit则只有JIT编译器。
图解
解释器
解释器是通过逐条解析字节码指令的方式让程序运作的。主要的优点是响应速度快,启动JVM后不需要等待太长的时间,当JVM虚拟机加载完对应的字节码文件后就开始解析执行,这点需求在客户端比较重要。但是由于它是逐条解析的,所以执行的效率并不理想,所以早期java的执行速度经常被程序员所诟病,原因是当时的执行引擎中只存在解释器。但随着java的不断演进,即时编译技术也逐渐成熟,加入了JIT编译器后,执行速度今非昔比。
JIT编译器
JIT编译器采用的方式是预先把指定的字节码指令编译成相应的机器码指令,并且缓存起来,后续的执行只需要直接去执行缓存好的机器码指令即可,无需再重复编译解析。缺点是执行前需要等待一段时间,也就是响应速度相对于解释器较慢,因为它要先编译。优点是一旦编译好对应的字节码指令后,后续的执行效率会比较高。JIT编译器又可以分为C1编译器,C2编译器对应的是客户端和服务端,jdk10还加入了Graal编译器。
C1
C1编译器对应的是Client模式的即时编译器,可以通过命令-client来切换。会对字节码进行简单和可靠的优化,耗时较短,但优化后的代码执行效率没C2的好。对于64位操作系统无效,默认是Server模式的编译器。
优化策略
- 方法内联
把相应的方法融合一起,减少栈帧的生成,减少方法间参数的传递以及跳转的次数 - 去虚拟化
针对唯一的实现类进行内联,也就是不去关联过多的类。 - 冗余消除
把运行过程中不被执行的代码进行折叠处理
C2
C2编译器对应的是Server模式的即时编译器,是用c++编写的,可以通过命令-server来切换。采用的是耗时较长的优化,以及激进的优化,优化后的代码执行效率较C1的要好。64位操作系统默认的就是Server模式,无法通过-client命令切换成Client模式。
优化策略
- 栈上分配
- 同步省略
- 标量替换
Graal
jdk10推出的全新编译器,编译效果在短短几年就追平了C2编译器,未来可期。
分层编译策略
程序执行时不开启性能监控可以触发C1编译器执行,如果开启性能监控,C2会根据性能监控信息进行激进优化。不过在jdk7以后,一旦显示执行命令 -server,将会默认开启性能监控,C1和C2互相协同运作。
解释器与JIT编译器并存
疑问
- 既然解释器效率比较慢,JIT编译器效率较快,那为什么不去除解释器,留下JIT编译器呢?
- 解释器看似有点“拖后腿”,但是在很多时候,响应速度是很关键的,比如用户在使用相应的产品时,往往要求产品能及时地给予响应,而只保留JIT编译器的话,则需要用户先等待一段时间,如果是比较复杂的业务,那等待的时间可能会比较长,那用户的体验是比较差的,即使后续执行的速度比较快,但用户可能只会记着响应慢这回事,所以比较好的方案是两者并存,优缺互补,而且解释器又可以充当后备方案,当JIT编译器激进优化失败后,解释器就充当后备角色。而JRockit只保留JIT编译器的原因是它是针对服务端的,所以启动后的响应速度没那么重要,反而是执行的效率要求比较高。
- 再举一个比较生动的例子,一个男的去约会,路程不算太远,但赴约时间快到,比较心急,他可以选择直接步行或跑步过去,也可以选择打车,这里的步行和跑步就好比解释器,马上就能出发,而打车就好比JIT编译器,需要先等车来才能出发,但一旦车来了,那速度肯定是比步行要快一点的,有一种比较巧妙的方案,就是既步行又打车,先出发,然后让车去预计的地点再上车,这就是比较理想的解释器和JIT编译器共存的方案。
- 什么时候采用解释器,什么时候采用JIT编译器?
刚开始时会先让解释器逐条解析执行,而JIT编译器的介入则取决于相应代码的执行频率,对于一些被频繁被调用的代码我们称之为热点代码,JIT编译器会把热点代码编译成机器码并缓存在方法区当中,后续需要调用热点代码时,只需执行缓存好的即可。
设置命令
- -Xint
完全采用解释器去执行。 - -Xcomp
完全采用JIT编译器去执行,出现问题时,解释器会介入。 - -Xmixed
采用解释器与JIT编译器并存的方式执行。
热点探测
- 被多次调用的方法,方法中循环次数较多的循环体都可以是热点代码,JIT编译器会把热点代码编译成相应的机器码指令,由于这个过程发生在方法的执行中,而方法执行对应的是栈的操作,所以这个过程又称为栈上替换,简称为OSR(On Stack Replacement)。
- HotSpot虚拟机的热点探测采用的是计数器的方式,HotSpot会针对每个方法创建两个计数器,分别是方法调用计数器和回边计数器,方法调用计数器记录的是方法被调用的次数,回边计数器记录的是方法中循环体循环的次数。
方法调用计数器
- 对于热点代码的定义有一个阈值,默认情况下,Client模式的值为1500,Server模式的值为10000
- 阈值可以通过参数命令 -XX:CompileThreshold 来设置
- 当一个方法被调用时,会去查看是否存在JIT编译器编译后的版本,如果存在,则优先去执行该版本,如果不存在,则照常执行,对应的方法调用计数器的值加1。然后把方法调用计数器值与回边计数器值的和与阈值比较,如果超过则向JIT编译器提出编译申请。
热度衰减
长期地执行并并不意味着所有的代码必然都会变成热点代码,因为还存在着热度衰减的机制。当一个方法对应的方法调用计数器无法在指定的时间限度内达到对应的阈值,那么就会让这个方法调用计数器的值减少一半,这个过程就称为热度衰减,对应的时间限度称为半衰周期。可以通过命令 -XX:-UseCounterDecay来关闭热度衰减,也可以通过命令 -XX:CounterHalfLifeTime来设置半衰周期。