许多人认为Java是一门解释执行的语言,由虚拟机解释执行class文件字节码。事实是Java是一门解释执行和编译执行并存的语言。JVM解释器让Java程序快速启动,编译器让Java程序高效运行,这是Java长久生存的一大重要原因。
解释器与编译器的关系
在一个Java程序执行时,首先通过javac把java文件编译为虚拟机可以识别的class文件。然后由JVM解释器解释class文件中的字节码,通过JVM把解释结果转变为机器码执行。这是我们通常所说的解释执行。
C++这类语言都是直接把代码编译为机器码执行交给计算机执行,这是我们通常所说的编译执行。
两者的最大区别就是是否存在一个中间处理器去参与由代码到机器码的流程。
解释执行有个缺点就是每次都要解释字节码然后才生成机器码让计算机执行,解释的过程会占用很多的时间。于是JVM中诞生了编译器,编译器可以通过热点代码探测技术,找到运行次数最多的代码,把这些代码及时编译为机器码,在下次调用这些代码时跳过解释的步骤,直接执行编译好的机器码,以达到加速运行时间的目的。
Java语言解释器与编译器并存的理念让Java程序得到了更加智能处理结果,是Java语言的特色,也是Java语言的优势。
解释器与编译器的工作模式
JVM分为Server模式和Client模式,对应的编译器也有C1(Client Complier)和C2(Server Complier)两个编译器。C1编译器注重编译的速度,C2编译器注重编译的质量。
不论在Server模式还是Client模式,解释器只有一个。早期的JVM中解释器只能和C1或C2中的一个混合运作(当然也可以通过设置虚拟机参数,强制只有编译器运作或只有解释器运作)。
在JDK1.7中,分层编译的加入,让两个编译器和一个解释器可以共同参与工作。
分层编译
第0层:编译器不工作,只有解释器解释执行程序,可触发第二层编译。
第1层:解释器和C1编译器共同工作。把热点字节码编译为机器码,并进行简单可靠的优化,如有必要会开启性能监测。可触发 第2层编译。
第2层(或第2层以上):解释器和C2编译器共同工作。把热点字节码编译为机器码,开启性能监测,并通过监测结果进行激进 优化。
在开启分层编译后,由于两个编译器的加入,许多代码会被多次编译。既通过C1得到了更高的编译速度,也通过C2得到了更高的编译质量,是JVM代码执行系统的一大进步。
激进优化
激进优化是不可靠的,所以进行激进优化时需要一个“逃生门”,通常由解释器担任,职责是在激进优化失败后继续完成任务。例如编译器优化措施中有一项优化方法名为“方法内联”。这个优化措施的行为大体是把方法体代码内嵌到它的调用者方法中,减少方法调用的成本。
但是Java的多态特性让这个优化有时不太可靠,因为在不具体执行方法之前,JVM并不知道该把多个子类中哪个类对应的方法体代码内联到调用者方法中。这时编译器会根据系统性能监测的结果,找到调用最多的方法体内码进行内嵌。
如果内嵌之后突然调用了一次其他子类的多态虚方法,这个内联方法就没有办法使用了,这时就出现了激进优化失败。这时只能通过预先设定的逃生门解决问题,由解释器不进行优化按照原始步骤解释执行代码完成这次任务。
编译对象的选择
编译器介入代码执行后,通过把字节码编译为机器码提升执行速度。被编译成机器码的主要有两项
被多次调用的方法和被多次执行的循环体。
被多次调用的方法
被多次调用的方法很好理解,一个方法被执行的多了,把他编译器机器码确实对提升代码整体运行速度很有帮助。
判断一个方法是否被多次调用主要通过采样探测法和计数器探测法来进行。
采样探测法:虚拟机周期性的对方法栈内的方法进行采样统计,如果多次发现同一个方法在栈顶,则说明这个方法频繁的被调用。这时就把它标记为一个多次调用方法等待编译器编译。
计数器探测法:给每个方法绑定一个计数器,每次方法被调用时计数器加一。然后给系统设定一个阈值,当一段时间内方法的调用次数超过阈值时把它标记为多次调用的方法,等待编译器编译。我们常用的HotSpot虚拟机就是采用这种方法探测热点代码。
方法调用流程
当编译器加入后,每个方法可以执行的代码就不止一份了。有一份字节码格式的方法代码,还有可能有多个机器码格式经过优化的方法代码。所以一个方法调用时会进行版本选择。
被多次执行执行的循环体
记录方法调用次数是用计数器实现的,记录循环执行次数也是用一种叫回边计数器实现的(这个计数器翻译的很棒啊)。在字节码中控制代码向后跳转的指令被称作“回边”。也就是说回边存在与循环体的末尾,所以回边计数器也存在于循环体的末尾。当回边计数器的值和方法调用计数器的和超过回边计数器的阈值时,就把这段循环体代码标记为多次执行循环体等待编译,并且把回边计数器的值下调一些让这次循环能按原规则执行完毕。
循环体单次执行流程