01、为什么使用即时编译器

Java程序最初是通过解释器进行解释执行的,当虚拟机发现某个方法或代码块的运行特别频繁时,就会把这些代码认定为“热点代码”。为了提高热点代码的执行效率,在运行时,虚拟机将会把这些代码编译成与本地平台相关的机器码,并进行各种层次的优化,完成这个任务的编译器称为即时编译器(Just In Time Compiler,简称JIT编译器)。

02、解释器与编译器两者各自的优势

当程序需要迅速启动和执行的时候,解释器可以首先发挥作用,省去编译的时间,立即执行。在程序运行后,随着时间的推移,编译器逐渐发挥作用,把越来越多的代码编译成本地代码之后,可以获取更高的执行效率。当程序运行环境中内存资源限制较大(如部分嵌入式系统中),可以使用解释执行节约内存,反之可以使用编译执行来提升效率。

03、分层编译

为了在程序启动响应速度与运行效率之间达到最佳平衡,HotSpot虚拟机还会逐渐启用分层编译的策略。分层编译根据编译器编译、优化的规模与耗时,划分出不同的编译层次。

实施分层编译后,Client Compiler和Server Compiler将会同时工作,许多代码都可能会被多次编译,用Client Compiler获取更高的编译速度,用Server
Compiler来获取更好的编译质量,在解释执行的时候也无须再承担收集性能监控信息的任务。

04、什么是栈上替换OSR?

在运行过程中被即时编译器编译的“热点代码”有两类,即:

  • 被多次调用的方法
  • 被多次执行的循环体

对于第一种情况,由于是方法调用触发的编译,因此编译器理所当然地会以整个方法作为编译对象,这种编译也是虚拟机中标准的JIT编译方式。而对于后一种情况,尽管编译动作是由循环体所触发的,但编译器依然会以整个方法(而不是单独的循环体)作为编译对象。这种编译方式因为编译发生在方法执行过程之中,因此形象地称之为栈上替换(On Stack Replacement,简称OSR编译,即方法栈帧还在栈上,方法就被替换了)。

05、热点探测判定方式

判断一段代码是不是热点代码,是不是需要触发即时编译,这样的行为称为热点探测。目前主要的热点探测判定方式有两种:

  • 基于采样的热点探测。
  • 基于计数器的热点探测。为每个方法准备了两类计数器:方法调用计数器和回边计数器。方法调用计数器用于统计方法被调用的次数。回边计数器用于统计一个方法中循环体代码执行的次数。在字节码中遇到控制流向后跳转的指令称为“回边”。

06、C1和C2的执行过程

对于Client Compiler(C1)来说,它是一个简单快速的三段式编译器,主要的关注点在于局部性的优化,而放弃了很多耗时的全局优化手段。

Server Compiler(C2)的寄存器分配器是一个全局着色分配器,它可以充分利用某些处理器架构(RISC)上的大寄存器。

07、编译优化技术

以编译方式执行本地代码比解释执行方式更快,因为除去虚拟机解释执行字节码时额外消耗时间的原因外,还有一个重要的原因就是虚拟机设计团队几乎把所有优化措施都集中在了即时编译器之中。一个简单的编译优化的例子——

首先需要明确的是,代码优化变换是建立在代码的某些中间表示或机器码之上,绝不是建立在Java源码之上的。

第一步进行方法内联,方法内联的重要性要高于其他优化措施,它主要目的有两个:一是去除方法调用的成本(如建立栈帧等),二是为其他优化建立良好的基础,方法内联膨胀之后,可以便于在更大范围上采取后续的优化手段,从而获取更好的优化效果。因此,各种编译器一般会把内联优化放在优化序列的最靠前位置。

第二步是进行冗余访问消除。

第三步是进行复写传播。

第四步进行无用代码消除。

08、公共子表达式消除

如果一个表达式E已经计算过了,并且从先前的计算到现在E中所有变量的值都没有发生变化,那么E的这次出现就成为了公共子表达式。对于这种表达式,没有必要花时间再对它进行计算,只需要直接用前面计算过的表达式结果替换E就可以了。

09、方法内联

把目标方法的代码“复制”到发起调用的方法之中,避免发生真实的方法调用。

10、逃逸分析

逃逸分析的基本行为就是分析对象动态作用域:当一个对象在方法中被定义后,它可能被外部方法所引用,例如作为调用参数传递到其他方法中,称为方法逃逸。甚至还有可能被外部线程访问到,譬如赋值给类变量或可以在其他线程中访问的实例变量,称为线程逃逸。

如果能证明一个对象不会逃逸到方法或线程之外,也就是别的方法或线程无法通过任何途径访问到这个对象,则可能为这个变量进行一些高效的优化。实现方法主要有:

1、栈上分配。

2、同步消除。

3、标量替换。