解释器与编译器

解释器与编译器两者各有优势:当程序需要迅速启动和执行的时候 解释器可以首先发挥作用 省去编译的时间 立即执行 在程序运行后 随着时间的推移 编译器逐渐发挥作用 把越来越多的代码编译成本地代码之后 可以获取更高的执行效率

HotSpot虚拟机中内置了两个即时编译器 分别称为Client Compiler和Server Compiler 或者简称为C1编译器和C2编译器(也叫Opto编译器) 目前主流的HotSpot虚拟机(Sun系列JDK 1.7及之前版本的虚拟机)中 默认采用解释器与其中一个编译器直接配合的方式工作 程序使用哪个编译器 取决于虚拟机运行的模式 HotSpot虚拟机会根据自身版本与宿主机器的硬件性能自动选择运行模式 用户也可以使用“-client”或“-server”参数去强制指定虚拟机运行在Client模式或Server模式

无论采用的编译器是Client Compiler还是Server Compiler 解释器与编译器搭配使用的方式在虚拟机中称为“混合模式”(Mixed Mode) 用户可以使用参数“-Xint”强制虚拟机运行于“解释模式”(Interpreted Mode) 这时编译器完全不介入工作 全部代码都使用解释方式执行 另外 也可以使用参数“-Xcomp”强制虚拟机运行于“编译模式”(Compiled Mode) 这时将优先采用编译方式执行程序 但是解释器仍然要在编译无法进行的情况下介入执行过程 可以通过虚拟机的“-version”命令的输出结果显示出这3种模式 java-version java-Xint-version java-Xcomp-version

编译对象与触发条件

在运行过程中会被即时编译器编译的“热点代码”有两类 即:被多次调用的方法 被多次执行的循环体

判断一段代码是不是热点代码 是不是需要触发即时编译 这样的行为称为热点探测(Hot Spot Detection) 其实进行热点探测并不一定要知道方法具体被调用了多少次 目前主要的热点探测判定方式有两种 分别如下
基于采样的热点探测(Sample Based Hot Spot Detection):采用这种方法的虚拟机会周期性地检查各个线程的栈顶 如果发现某个(或某些)方法经常出现在栈顶 那这个方法就是“热点方法” 基于采样的热点探测的好处是实现简单 高效 还可以很容易地获取方法调用关系(将调用堆栈展开即可) 缺点是很难精确地确认一个方法的热度 容易因为受到线程阻塞或别的外界因素的影响而扰乱热点探测
基于计数器的热点探测(Counter Based Hot Spot Detection):采用这种方法的虚拟机会为每个方法(甚至是代码块)建立计数器 统计方法的执行次数 如果执行次数超过一定的阈值就认为它是“热点方法” 这种统计方法实现起来麻烦一些 需要为每个方法建立并维护计数器 而且不能直接获取到方法的调用关系 但是它的统计结果相对来说更加精确和严谨

在HotSpot虚拟机中使用的是第二种——基于计数器的热点探测方法 因此它为每个方法准备了两类计数器:方法调用计数器(Invocation Counter)和回边计数器(Back Edge Counter)
在确定虚拟机运行参数的前提下 这两个计数器都有一个确定的阈值 当计数器超过阈值溢出了 就会触发JIT编译

我们首先来看看方法调用计数器 顾名思义 这个计数器就用于统计方法被调用的次数 它的默认阈值在Client模式下是1500次 在Server模式下是10 000次 这个阈值可以通过虚拟机参数-XX:CompileThreshold来人为设定

如果不做任何设置 方法调用计数器统计的并不是方法被调用的绝对次数 而是一个相对的执行频率 即一段时间之内方法被调用的次数 当超过一定的时间限度 如果方法的调用次数仍然不足以让它提交给即时编译器编译 那这个方法的调用计数器就会被减少一半 这个过程称为方法调用计数器热度的衰减(Counter Decay) 而这段时间就称为此方法统计的半衰周期(Counter Half Life Time) 进行热度衰减的动作是在虚拟机进行垃圾收集时顺便进行的 可以使用虚拟机参数-XX:-UseCounterDecay来关闭热度衰减 让方法计数器统计方法调用的绝对次数 这样 只要系统运行时间足够长 绝大部分方法都会被编译成本地代码 另外 可以使用-XX:CounterHalfLifeTime参数设置半衰周期的时间 单位是秒

现在我们再来看看另外一个计数器——回边计数器 它的作用是统计一个方法中循环体代码执行的次数 在字节码中遇到控制流向后跳转的指令称为“回边”(Back Edge) 显然 建立回边计数器统计的目的就是为了触发OSR编译

关于回边计数器的阈值 虽然HotSpot虚拟机也提供了一个类似于方法调用计数器阈值-XX:CompileThreshold的参数-XX:BackEdgeThreshold供用户设置 但是当前的虚拟机实际上并未使用此参数 因此我们需要设置另外一个参数-XX:OnStackReplacePercentage来间接调整回边计数器的阈值 其计算公式如下
虚拟机运行在Client模式下 回边计数器阈值计算公式为:
方法调用计数器阈值(CompileThreshold)×OSR比率(OnStackReplacePercentage)/100
其中OnStackReplacePercentage默认值为933 如果都取默认值 那Client模式虚拟机的回边计数器的阈值为13995
虚拟机运行在Server模式下 回边计数器阈值的计算公式为:
方法调用计数器阈值(CompileThreshold)×(OSR比率(OnStackReplacePercentage)-解释器监控比率(InterpreterProfilePercentage)/100
其中OnStackReplacePercentage默认值为140 InterpreterProfilePercentage默认值为33 如果都取默认值 那Server模式虚拟机回边计数器的阈值为10700

编译过程

在默认设置下 无论是方法调用产生的即时编译请求 还是OSR编译请求 虚拟机在代码编译器还未完成之前 都仍然将按照解释方式继续执行 而编译动作则在后台的编译线程中进行 用户可以通过参数-XX:-BackgroundCompilation来禁止后台编译 在禁止后台编译后 一旦达到JIT的编译条件 执行线程向虚拟机提交编译请求后将会一直等待 直到编译过程完成后再开始执行编译器输出的本地代码

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)等

查看及分析即时编译结果

要知道某个方法是否被编译过 可以使用参数-XX:+PrintCompilation要求虚拟机在即时编译时将被编译成本地代码的方法名称打印出来 其中带有“%”的输出说明是由回边计数器触发的OSR编译
还可以加上参数-XX:+PrintInlining要求虚拟机输出方法内联信息

除了查看哪些方法被编译之外 还可以进一步查看即时编译器生成的机器码内容 在为虚拟机安装了反汇编适配器之后 就可以使用-XX:+PrintAssembly参数要求虚拟机打印编译方法的汇编代码了 如果没有HSDIS插件支持 也可以使用-XX:+PrintOptoAssembly(用于Server VM)或-XX:+PrintLIR(用于Client VM)来输出比较接近最终结果的中间代码表示 从阅读角度来说 使用-XX:+PrintOptoAssembly参数输出的伪汇编结果包含了更多的信息(主要是注释)

前面提到的使用-XX:+PrintAssembly参数输出反汇编信息需要Debug或者FastDebug版的虚拟机才能直接支持 如果使用Product版的虚拟机 则需要加入参数-XX:+UnlockDiagnosticVMOptions打开虚拟机诊断模式后才能使用

如果除了本地代码的生成结果外 还想再进一步跟踪本地代码生成的具体过程 那还可以使用参数-XX:+PrintCFGToFile(使用Client Compiler)或-XX:PrintIdealGraphFile(使用Server Compiler)令虚拟机将编译过程中各个阶段的数据(例如 对C1编译器来说 包括字
节码 HIR生成 LIR生成 寄存器分配过程 本地代码生成等数据)输出到文件中 然后使用Java HotSpot Client Compiler Visualizer(用于分析Client Compiler)或Ideal Graph Visualizer(用于分析Server Compiler)打开这些数据文件进行分析

Server Compiler的中间代码表示是一种名为Ideal的SSA形式程序依赖图(Program Dependence Graph) 在运行Java程序的JVM参数中加入“-XX:PrintIdealGraphLevel=2-XX:PrintIdealGraphFile=ideal.xml” 编译后将产生一个名为ideal.xml的文件 它包含了Server Compiler编译代码的过程信息 可以使用Ideal Graph Visualizer对这些信息进行分析