HotSpot虚拟机内的即时编译器
最近在看周志明老师的《深入理解Java虚拟机》这本书,看了之后感觉收获挺多的,于是就写点读书总结吧。
先看如下问题:
- 1、 为何HotSpot虚拟机要使用解释器与编译器并存的架构?
- 2、 为何HotSpot虚拟机要实现两个不同的即时编译器?
- 3、 程序何时使用解释器执行?何时使用编译器执行?
- 4、 那些程序代码会被编译为本地代码?如何便以为本地代码?
Java程序最初是仅仅通过解释器解释执行的,即对字节码逐条解释执行,这种方式的执行速度相对会比较慢,尤其当某个方法或代码块运行的特别频繁时,这种方式的执行效率就显得很低。于是后来在虚拟机中引入了JIT编译器(即时编译器),当虚拟机发现某个方法或代码块运行特别频繁时,就会把这些代码认定为“Hot Spot Code”(热点代码),为了提高热点代码的执行效率,在运行时,虚拟机将会把这些代码编译成与本地平台相关的机器码,并进行各层次的优化,完成这项任务的正是JIT编译器。
下面以问答的形式进行叙述
- 1、为什么HotSpot虚拟机要使用解释器与编译器并存的架构??
现在主流的商用虚拟机(如Sun HotSpot、IBM J9)中几乎都同时包含解释器和编译器(三大商用虚拟机之一的JRockit是个例外,它内部没有解释器,因此会有启动相应时间长之类的缺点,但它主要是面向服务端的应用,这类应用一般不会重点关注启动时间)。二者各有优势:当程序需要迅速启动和执行时,解释器可以首先发挥作用,省去编译的时间,立即执行;当程序运行后,随着时间的推移,编译器逐渐会发挥作用,把越来越多的代码编译成本地代码后,可以获取更高的执行效率。解释执行可以节约内存,而编译执行可以提升效率。在整个虚拟机执行架构中,解释器与编译器经常配合工作。
- 2、为什么HotSpot虚拟机要实现两个不同的即时编译器的原因???
HotSpot虚拟机中内置了两个即时编译器:Client Complier和Server Complier,简称为C1、C2编译器,分别用在客户端和服务端,目前主流的HotSpot虚拟机中默认是采用解释器与其中一个编译器直接配合的方式工作。程序使用哪个编译器,取决于虚拟机运行的模式。HotSpot虚拟机会根据自身版本与宿主及其的硬件性能自动选择运行模式,用户也可以使用“-client”或“-server”参数去强制指定虚拟机运行在Client模式或Server模式。用Client Complier获取更高的编译速度,用Server Complier 来获取更好的编译质量。
- 4、 那些程序代码会被编译为本地代码?如何编译为本地代码?
程序中的代码只有是热点代码时,才会编译为本地代码,那么什么是热点代码呢?
运行过程中会被即时编译器编译的“热点代码”有两类:
- 被多次调用的方法。
- 被多次调用的循环体。
两种情况,编译器都是以整个方法作为编译对象,这种编译也是虚拟机中标准的编译方式。
如何判断方法或一段代码或是不是热点代码呢
要知道方法或一段代码是不是热点代码,是不是需要触发即时编译,需要进行Hot Spot Detection(热点探测)。目前主要的热点 判定方式有以下两种:
(1)基于采样的热点探测
采用这种方法的虚拟机会周期性地检查各个线程的栈顶,如果发现某些方法经常出现在栈顶,那这段方法代码就是“热点代码”。这种探测方法的好处是实现简单高效,还可以很容易地获取方法调用关系,缺点是很难精确地确认一个方法的热度,容易因为受到线程阻塞或别的外界因素的影响而扰乱热点探测。
(2)基于计数器的热点探测
采用这种方法的虚拟机会为每个方法,甚至是代码块建立计数器,统计方法的执行次数,如果执行次数超过一定的阀值,就认为它是“热点方法”。这种统计方法实现复杂一些,需要为每个方法建立并维护计数器,而且不能直接获取到方法的调用关系,但是它的统计结果相对更加精确严谨。
HotSpot虚拟机中使用的是哪钟热点检测方式呢
在HotSpot虚拟机中使用的是第二种——基于计数器的热点探测方法,因此它为每个方法准备了两个计数器:方法调用计数器和回边计数器。在确定虚拟机运行参数的前提下,这两个计数器都有一个确定的阈值,当计数器超过阈值溢出了,就会触发JIT编译。
方法调用计数器:这个计数器用于统计方法被调用的次数。
- 3、 程序何时使用解释器执行?何时使用编译器执行?
当一个方法被调用时,会先检查该方法是否存在被JIT编译过的版本,如果存在,则优先使用编译后的本地代码来执行。如果不存在已被编译过的版本,则将此方法的调用计数器值加1,然后半段方法调用计数器与回边计数器值之后是否超过方法调用计数器的阈值。如果超过阈值,那么将会想即时编译器提交一个该方法的代码编译请求。
如果不做任何设置,执行引擎并不会同步等待编译请求完成,而是继续进行解释器按照解释方式执行字节码,直到提交的请求被编译器编译完成。当编译工作完成之后,这个方法的调用入口地址就会系统自动改写成新的,下一次调用该方法时就会使用已编译的版本。
回边计数器:统计一个方法中循环体代码执行的次数。应用与循环体的回边计数器与方法计数器的原理一样,这里就不在叙述。