### 1、编译器概念
Java语言的“编译期”其实是一段“不确定”的操作过程,因为它可能是指一个前端编译器(其实叫“编译器的前端”更准确一些)把.java文件转变成.class文件的过程;也可能是指虚拟机的后端运行期编译器(JIT编译器,Just In Time Compiler)把字节码转变成机器码的过程;·还可能是指使用静态提前编译器(AOT编译器,Ahead of Time Compiler)直接把.java文件编译成本地机器代码的过程。不同编译器的实现代表有:
- 前端编译器: Sun的 Javac.Eclipse JDT中的增量式编译器(ECJ)。
- JIT编译器:HotSpot VM的Cl、C2编译器。
- AOT编译器: GNU Compiler for the Java(GCJ)、Excelsior JET.
### 2、何时使用JIT编译器?
是否需要启动JIT编译器将字节码直接编译为对应平台的本地机器指令,需要根据代码被调用执行的频率而定。关于那些需要被编译为本地代码的字节码,也被称之为“热点代码”,JIT编译器在运行时会针对那些频繁被调用的“热点代码”做出深度优化,将其直接编译为对应平台的本地机器指令,以此提升Java程序的执行性能。
一个被多次调用的方法,或者是一个方法体内部循环次数较多的循环体都可以被称之为“热点代码,因此都可以通过IT编译器编译为本地机器指令。由于这种编译方
式发生在方法的执行过程中,因此也被称之为**栈上替换**,或简称为**OSR**(On Stack Replacement)编译。一个方法究竟要被调用多少次,或者一个循环体究竟需要执行多少次循环才可以达到这个标准?必然需要一个明确的阈值,JIT编译器才会将这些“热点代码”编译为本地机器指令执行。这里主要依靠热点探测功能。目前HotSpot VM所采用的热点探测方式是基于计数器的热点探测。采用基于计数器的热点探测,HotSpot VM将会为每一个方法都建立2个不同类型的计数器,分别为**方法调用计数器**(Invocation Counter)和**回边计数器**(Back Edge Counter)。
- 方法调用计数器用于统计方法的调用次数
- 回边计数器则用于统计循环体执行的循环次数
#### 2.1、方法调用计数器
这个计数器就用于统计方法被调用的次数,它的默认阈值在Client模式下是1500 次,在Server模式下是10000次。超过这个阈值,就会触发JIT编译。
- 这个阈值可以通过虚拟机参数-XX:CompileThreshold来人为设定。
- 当一个方法被调用时,会先检查该方法是否存在被JIT编译过的版本,如果存在,则优先使用编译后的本地代码来执行。如果不存在已被编译过的版本,则将此方法的调用计数器值加1,然后判断方法调用计数器与回边计数器值之和是否超过方法调用计数器的阈值。如果已超过阈值,那么将会向即时编译器提交一个该方法的代码编译请求。

#### 2.2、热度衰减
如果不做任何设置,方法调用计数器统计的并不是方法被调用的绝对次数,而是一个相对的执行频率,即一段时间之内方法被调用的次数。当超过一定的时间限度,如果方法的调用次数仍然不足以让它提交给即时编译器编译,那这个方法的调用计数器就会被减少一半,这个过程称为方法调用计数器的**热度衰减**(Counter Decay),而这段时间就称为此方法统计的**半衰周期**(Counter Half Life Time)。
进行热度衰减的动作是在虚拟机进行垃圾收集时顺便进行的,可以使用虚拟机参数-XX:-UseCounterDecay来关闭热度衰减,让方法计数器统计方法调用的绝对次数,这样,只要系统运行时间足够长,绝大部分方法都会被编译成本地代码。另外,可以使用-XX:CounterHalfLifeTime参数设置半衰周期的时间,单位是秒。
#### 2.3、回边计数器
它的作用是统计一个方法中循环体代码执行的次数,在字节码中遇到控制流向后跳转的指令称为“回边”(Back Edge)。显然,建立回边计数器统计的目的就是为了触发OSR编译。

### 3、如何设置解释器、编译器协同工作?
缺省情况下HotSpot VM是采用解释器与即时编译器并存的架构,当然开发人员可以根据具体的应用场景,通过命令显式地为Java虚拟机指定在运行时到底是完全采用解释器执行,还是完全采用即时编译器执行。如下所示:
- -Xint:完全采用解释器模式执行程序;
- -Xcomp:完全采用即时编译器模式执行程序。如果即时编译出现问题,解释器会介入执行。
- -Xmixed:采用解释器+即时编译器的混合模式共同执行程序。
### 4、C1与C2编译器
在HotSpot VM中内嵌有两个JIT编译器,分别为Client Compiler和Server Compiler,但大多数情况下我们简称为c1编译器和C2编译器。开发人员可以通过如下命令显式指定Java虚拟机在运行时到底使用哪一种即时编译器,如下所示:
- -client:指定Java虚拟机运行在client模式下,并使用C1编译器;
C1编译器会对字节码进行简单和可靠的优化,耗时短。以达到更快的编译速度。
- -server:指定Java虚拟机运行在Server模式下,并使用C2编译器。
C2进行耗时较长的优化,以及激进优化。但优化的代码执行效率更高。
在64位机器上,默认的只支持server模式。如果在64位机器上设置 “-client”,会被忽略。
**C1和C2编译器不同的优化策略:**
- 在不同的编译器上有不同的优化策略,c1编译器上主要有方法内联,去虚拟化、冗余消除。
- 方法内联:将引用的函数代码编译到引用点处,这样可以减少栈帧的生成,减少参数传递以及跳转过程。
- 去虚拟化:对唯一的实现类进行内联。
- 冗余消除:在运行期间把一些不会执行的代码折叠掉
- C2的优化主要是在全局层面,逃逸分析是优化的基础。基于逃逸分析在C2上有如下几种优化:
- 标量替换:用标量值代替聚合对象的属性值。
- 栈上分配:对于未逃逸的对象分配对象在栈而不是堆。
- 同步消除:清除同步操作,通常指synchronized。
**分层编译策略:**
分层编译(Tiered Compilation)策略:程序解释执行(不开启性能监控)可以触发c1编译,将字节码编译成机器码,可以进行简单优化,也可以加上性能监控,C2编译会根据性能监控信息进行激进优化。
不过在Java7版本之后,一旦开发人员在程序中显式指定命令“-server"时,默认将会开启分层编译策略由c1编译器和c2编译器相互协作共同来执行编译任务。
**总结:**
- 一般来讲,JIT编译出来的机器码性能比解释器高。
- C2编译器启动时长比C1编译器慢,系统稳定执行以后,C2编译器执行速度远远快于C1编译器。

循序渐进Java虚拟机-JIT编译器