JVM知识点总结(四)——即时编译(JIT)

一些代表性的编译器:
- 前端编译器:Sun的Javac、Eclipse JDT中的增量式编译器(ECJ)。
- JIT编译器:Hotspot VM的C1、C2编译器。
- AOT(Ahead of Time):GCT、Excelsior JET。

——Java三种编译方式

javac的任务就是将Java源代码编译成Java字节码,也就是JVM能够识别的二进制代码,从表面看是将.java文件转化为.class文件。而实际上是将Java源代码转化成一连串二进制数字,这些二进制数字是有格式的,只有JVM能够真确的识别他们到底代表什么意思。这部分的编译过程可以参考“编译原理”中的相关内容,包括词法分析、语法分析、语义分析等等。

解释执行与编译执行

解释型代码的优势是可移植,只要有合适的解释器,代码可以在任意平台上运行,而且解释型代码启动速度总是要比编译型要快;而编译型代码是将代码编译成针对特定平台的机器码,是不可移植的,且编译型代码由于编译成了针对机器的机器码,它的执行效率总是高于解释型代码。Java试图结合两者的优点,在最初执行时,程序是解释执行的,当虚拟机发现某些代码执行特别频繁时,会将这些代码认定为“热点代码”,并将这些代码编译成平台相关的机器码以提高执行效率。由于这个编译是在程序执行时进行的,因此被称为“即时编译”(即Just-in-Time,JIT)。

在Java中,这里的解释执行和编译执行,都是针对已经经过前端编译器(javac)编译后的.class文件来说的,因为.class才是JVM唯一识别的代码。

热点编译

Hotspot虚拟机的名字来自于它看待代码编译的方式,本文的虚拟机指的都是Hotspot虚拟机。在程序中通常只有一部分代码会被经常执行,这些代码被称作“热点代码(Hot Spot Code)”,提高这些代码的执行效率,是提高应用程序性能的关键。虚拟机在执行代码的时候不会立即编译代码,原因有两个:对于执行一次的代码,解释执行比编译后再执行的速度快;虚拟机执行特定方法的次数越多,它对代码的了解越多,也能进行更深的优化。

Hotspot中的编译器

JVM知识点总结(四)——即时编译(JIT)_第1张图片
Hotspot虚拟机中内置了两个即时编译器,Client Complier和Server Complier,或者也成为C1编译器和C2编译器(C2可以看做是C1的激进模式),用户可以使用“-client”或“-server”参数去强制指定虚拟机运行在Client模式或Server模式,但是这个参数不会影响解释器的运行。无论使用的是C1还是C2,解释器与编译器搭配使用的方式在虚拟机中称为“混合模式 ”(Mixed Mode),用户可以使用“-Xint”强制虚拟机运行于“解释模式”(Interpreted Mode);“-Xcomp”强制虚拟机运行于“编译模式”(Complied Mode)。
前面说过,程序不会一开始就编译执行,只有执行次数达到一定的阈值才会进行编译优化。在Hotspot中使用的是基于计数器的热点探测方法来判断一段代码是不是热点代码,该方法为每个方法准备了两类计数器:方法调用计数器和会变计数器。两个计数器都有一个确定的阈值,当计数器超出阈值,就会触发JIT编译。

编译触发条件

在运行过程中会被即时编译器编译的“热点代码”有两类:被多次调用的方法;被多次执行的循环体。这个“多次”该通过什么途径确定?Hotspot使用的是基于计数器的热点探测技术。

方法调用计数器

JVM知识点总结(四)——即时编译(JIT)_第2张图片
方法调用计数器在Client模式下的阈值为1500次,Server模式为10000次,可使用-XX:CompileThreshold参数来设定。如果不做任何限制,方法调用计数器统计的不是方法调用的绝对次数,而是在一定时间之内的调用次数,即执行频率,当超过一定时间限度,方法调用次数不足以让它提交给即时编译器,那这个方法的调用计数器会被减少一半,这个过程称为方法调用计数器热度的衰减。该过程是在垃圾回收时顺便进行的,-XX:UseCounterDecay可以关闭热度衰减,-XX:CounterHalfLifeTime设置半衰周期的时间,单位秒。

回边计数器

回边计数器是用来统计一个方法中回边代码执行的次数,回边一定是循环体,但是循环体不一定是回边。回边计数器的阈值没有直接参数来进行设计,可使用-XX:OnStackReplacePercentage间接设置,这里有一个计算公式:Client模式,方法调用计数器阈值×OSR比率(OnStackReplacePercentage)/100;Server模式,方法调用计数器阈值×(OSR比率-解释器监控比率(InterpreterProfilePercentage))/100
JVM知识点总结(四)——即时编译(JIT)_第3张图片
与方法调用计数器不同,回边计数器不存在热度衰减过程,因此回边计数器统计的就是该方法循环执行的绝对次数。当计数器溢出的时候,它会把方法计数器的值也调整到溢出,这样下次再进入该方法的时候就会执行标准编译过程。

编译缓存不足

虚拟机在进行编译时,会将编译后的代码保存在缓存中。代码缓存的大小固定,所以一旦填满,就无法再编译更多代码了。代码缓存填满时,虚拟机会发出警告,需要注意的是,这些警告是在程序运行中出现的。

分层编译

JDK6时期出现分层编译的概念,JDK7时在-server模式下才会开启,JDK8时分层编译默认为开启。这里的划分方式各个书中的版本不同,《深入理解Java虚拟机(第2版)》分为3层(0,1级别不变,级别2化为C2编译),《Java性能权威指南》分为5层:
- 第0层,程序解释执行,解释器不开启性能监控功能,可触发第1层编译。
- 第1层,简单C1编译代码。
- 第2层,受限的C1编译代码。
- 第3层,完全C1编译代码。
- 第4层,C2编译代码。
所有方法都是从级别0开始,多数代码第一次编译的级别是3。级别2产生的情况可能为:C2编译队列满了;C1编译器全忙;有时候有些不太重要的方法会从级别2开始编译(而不是3)。前两种情况下的级别2编译都是有大概率转到级别4编译,而最后一种情况会转为级别1。我的个人理解级别2是级别3的备胎,正常编译的话只用级别3就行了,一些特殊情况就需要备胎来进行协助了。
C1是一个简单快速的三段式编译器,主要的关注点在于局部性的优化,放弃了许多耗时较长的全局优化手段。
C2会执行所有经典的优化动作:无用代码消除、循环展开、循环表达式外提、消除公共子表达式、常量传播、基本块重排等。

参考链接:

  1. 《深入理解Java虚拟机(第2版)》
  2. 《Java性能权威指南》

你可能感兴趣的:(Java精华笔记)