在部分的商用虚拟机中,Java 程序最初是经过解释器( Interpreter )进行解释执行的,当虚拟机发现某个方法或代码块的运行特别频繁的时候,就会把这些代码认定为“热点代码”。为了提升热点代码的执行效率,在运行时,即时编译器(Just In Time Compiler )会把这些代码编译成与本地平台相关的机器码,并进行各类层次的优化。前端
解释器和编译器各有各的优势:算法
解释器优势:当程序须要迅速启动的时候,解释器能够首先发挥做用,省去了编译的时间,当即执行。解释执行占用更小的内存空间。同时,当编译器进行的激进优化失败的时候,还能够进行逆优化来恢复到解释执行的状态。express
编译器优势:在程序运行时,随着时间的推移,编译器逐渐发挥做用,把愈来愈多的代码编译成本地代码以后,能够得到更高的执行效率。后端
所以,整个虚拟机执行架构中,解释器与编译器常常配合工做,以下图所示。架构
HotSpot中内置了两个即时编译器,分别称为 Client Compiler和 Server Compiler ,或者简称为 C1 编译器和 C2 编译器。目前的 HotSpot 编译器默认的是解释器和其中一个即时编译器配合的方式工做,具体是哪个编译器,取决于虚拟机运行的模式,HotSpot 虚拟机会根据自身版本与计算机的硬件性能自动选择运行模式,用户也可使用 -client 和 -server 参数强制指定虚拟机运行在 Client 模式或者 Server 模式。这种配合使用的方式称为“混合模式”(Mixed Mode),用户可使用参数 -Xint 强制虚拟机运行于 “解释模式”(Interpreted Mode),这时候编译器彻底不介入工做。另外,使用 -Xcomp 强制虚拟机运行于 “编译模式”(Compiled Mode),这时候将优先采用编译方式执行,可是解释器仍然要在编译没法进行的状况下接入执行过程。经过虚拟机 -version 命令能够查看当前默认的运行模式。oop
在运行过程当中会被即时编译的“热点代码”有两类,即:性能
- 被屡次调用的方法
- 被屡次执行的循环体
对于第一种,编译器会将整个方法做为编译对象,这也是标准的JIT 编译方式。对于第二种是由循环体出发的,可是编译器依然会以整个方法做为编译对象,由于发生在方法执行过程当中,称为栈上替换。
判断一段代码是不是热点代码,是否是须要出发即时编译,这样的行为称为热点探测(Hot Spot Detection),探测算法有两种,分别为。优化
- 基于采样的热点探测(Sample Based Hot Spot Detection):虚拟机会周期的对各个线程栈顶进行检查,若是某些方法常常出如今栈顶,这个方法就是“热点方法”。好处是实现简单、高效,很容易获取方法调用关系。缺点是很难确认方法的reduce,容易受到线程阻塞或其余外因扰乱。
- 基于计数器的热点探测(Counter Based Hot Spot Detection):为每一个方法(甚至是代码块)创建计数器,执行次数超过阈值就认为是“热点方法”。优势是统计结果精确严谨。缺点是实现麻烦,不能直接获取方法的调用关系。
HotSpot 使用的是第二种-基于技术其的热点探测,而且有两类计数器:方法调用计数器(Invocation Counter )和回边计数器(Back Edge Counter )。线程
这两个计数器都有一个肯定的阈值,超事后便会触发 JIT 编译。server
首先是方法调用计数器。Client 模式下默认阈值是 1500 次,在 Server 模式下是 10000次,这个阈值能够经过 -XX:CompileThreadhold 来人为设定。若是不作任何设置,方法调用计数器统计的并非方法被调用的绝对次数,而是一个相对的执行频率,即一段时间以内的方法被调用的次数。当超过必定的时间限度,若是方法的调用次数仍然不足以让它提交给即时编译器编译,那么这个方法的调用计数器就会被减小一半,这个过程称为方法调用计数器热度的衰减(Counter Decay),而这段时间就成为此方法的统计的半衰周期( Counter Half Life Time)。进行热度衰减的动做是在虚拟机进行垃圾收集时顺便进行的,可使用虚拟机参数 -XX:CounterHalfLifeTime 参数设置半衰周期的时间,单位是秒。整个 JIT 编译的交互过程以下图。
第二个回边计数器,做用是统计一个方法中循环体代码执行的次数,在字节码中遇到控制流向后跳转的指令称为“回边”( Back Edge )。显然,创建回边计数器统计的目的就是为了触发 OSR 编译。关于这个计数器的阈值, HotSpot 提供了 -XX:BackEdgeThreshold 供用户设置,可是当前的虚拟机实际上使用了 -XX:OnStackReplacePercentage 来简介调整阈值,计算公式以下:
在 Client 模式下, 公式为 方法调用计数器阈值(CompileThreshold)X OSR 比率(OnStackReplacePercentage)/ 100 。其中 OSR 比率默认为 933,那么,回边计数器的阈值为 13995。
在 Server 模式下,公式为 方法调用计数器阈值(Compile Threashold)X (OSR (OnStackReplacePercentage)- 解释器监控比率 (InterpreterProfilePercent))/100
其中 onStackReplacePercentage 默认值为 140,InterpreterProfilePercentage 默认值为 33,若是都取默认值,那么 Server 模式虚拟机回边计数器阈值为 10700 。
执行过程,以下图。
默认状况下,不管是方法调用产生的即时编译请求,仍是 OSR 请求,虚拟机在代码编译器还未完成以前,都仍然将按照解释方式继续执行,而编译动做则在后台的编译线程中进行,用户能够经过参数 -XX:-BackgroundCompilation 来禁止后台编译,这样,一旦达到 JIT 的编译条件,执行线程向虚拟机提交便已请求以后便会一直等待,直到编译过程完成后再开始执行编译器输出的本地代码。
对于 Client 模式而言
它是一个简单快速的三段式编译器,主要关注点在于局部的优化,放弃了许多耗时较长的全局优化手段。
- 第一阶段,一个平台独立的前端将字节码构形成一种高级中间代码表示(High-Level Intermediate Representaion , HIR)。在此以前,编译器会在字节码上完成一部分基础优化,如 方法内联,常量传播等优化。
- 第二阶段,一个平台相关的后端从 HIR 中产生低级中间代码表示(Low-Level Intermediate Representation ,LIR),而在此以前会在 HIR 上完成另一些优化,如空值检查消除,范围检查消除等,让HIR 更为高效。
- 第三阶段,在平台相关的后端使用线性扫描算法(Linear Scan Register Allocation)在 LIR 上分配寄存器,作窥孔(Peephole)优化,而后产生机器码。
Client Compiler 的大体执行过程以下图所示:
对于 Server Compiler 模式而言
它是专门面向服务端的典型应用,并为服务端的性能配置特别调整过的编译器,也是一个充分优化过的高级编译器,几乎能达到 GNU C++ 编译器使用-O2 参数时的优化强度,它会执行全部的经典的优化动做,如 无用代码消除(Dead Code Elimination)、循环展开(Loop Unrolling)、循环表达式外提(Loop Expression Hoisting)、消除公共子表达式(Common Subexpression Elimination)、常量传播(Constant Propagation)、基本块冲排序(Basic Block Reordering)等,还会实施一些与 Java 语言特性密切相关的优化技术,如范围检查消除(Range Check Elimination)、空值检查消除(Null Check Elimination ,不过并不是全部的空值检查消除都是依赖编译器优化的,有一些是在代码运行过程当中自动优化 了)等。另外,还可能根据解释器或Client Compiler 提供的性能监控信息,进行一些不稳定的激进优化,如 守护内联(Guarded Inlining)、分支频率预测(Branch Frequency Prediction)等。
Server Compiler 编译器能够充分利用某些处理器架构,如(RISC)上的大寄存器集合。从即时编译的角度来看, Server Compiler 无疑是比较缓慢的,但它的便以速度仍远远超过传统的静态优化编译器,并且它相对于 Client Compiler编译输出的代码质量有所提升,能够减小本地代码的执行时间,从而抵消了额外的编译时间开销,因此也有不少非服务端的应用选择使用 Server 模式的虚拟机运行。