JVM性能分析

发布时间 2023-11-15 11:20:53作者: 奥奥呀

JIT
在谈到 Java 的编译机制的时候,其实应该按时期,分为两个阶段。一个是 javac 指令将 Java 源码变为 Java 字节码的静态编译过程。另一个是 Java 字节码编译为本地机器码的过程,并且因为这个过程是在程序运行时期完成的所以称之为即时编译(JIT),下面我们讨论的编译也都是指“即时编译”过程。

解释器
java作为一种跨平台的语言实现了一次编译到处运行的特性,这也就决定了它编译出来的不是机器码而是特定的字节码。解释器(各平台不同)就是将字节码解释为机器指令,调用操作系统来完成程序的执行。

编译器
解释器虽然实现了跨平台的特性,但是解释执行的效率是很低的,是以牺牲性能为代价来换取的跨平台特性。所以 JVM 发现某个方法或者代码块的运行特别频繁时,就会把这些代码认定为“热点代码”(Hot Spot Code,不知道Sun的虚拟机命名是否跟这个有联系)。为了提高热点代码的执行效率,在运行时,虚拟机就会将这些代码翻译成与本地平台相关的机器码,并进行各种层次的优化,完成这个任务的就是编译器,被称为即时编译器(Just In Time Compiler,简称为JIT)。

HotSpot 虚拟机内置两个即时编译器,称为 Client Compiler 和 Server Compiler,分别简称为 C1,C2。

C1 编译器是一个简单快速的编译器,主要的关注点在于局部性的优化,适用于执行时间较短或对启动性能有要求的程序
C2 编译器是为长期运行的应用程序做性能调优的编译器,适用于执行时间较长或对峰值性能有要求的程序。可能会对代码进行激进的优化来获取更好的性能,这些优化往往伴随着耗时较长的代码分析,同时会设定“逃生门”在激进优化不成立的时候回退到 C1 编译器或者解释器继续执行
分层编译
由于即时编译器编译本地代码需要占用程序运行时间,而要编译出优化程度较高的代码,所花费的时间可能更多。为了在程序启动速度与运行效率之间达到平衡,HotSpot 虚拟机启用了分层编译(Tiered Compilation)策略。

在分层编译中,会同时使用两个编译器。当 C2 编译器在等待并分析一些代码片段来收集信息的时候,C1 编译器首先开始编译。这使得 C1 编译器能够快速的提高性能;而 C2 编译器将能够更好地提高性能,因为它拥有有热点方法更好的信息。分层编译在 JDK1.6 时期出现,在 JDK1.7 的 Server 模式中作为默认编译策略开启。

profiling 就是收集能够反映程序执行状态的数据。其中最基本的统计数据就是方法的调用次数,以及循环回边的执行次数。

通常情况下,C2 代码的执行效率要比 C1 代码的高出 30% 以上。对于 C1 代码的三种状态,按执行效率从高至低则是 1 层 > 2 层 > 3 层。其中 1 层的性能比 2 层的稍微高一些,而 2 层的性能又比 3 层高出 30%。这是因为 profiling 越多,其额外的性能开销越大。

这 5 个层次的执行状态中,1 层和 4 层为终止状态。当一个方法被终止状态编译过后,如果编译后的代码并没有失效,那么 Java 虚拟机将不再次发出该方法的编译请求的。

通常情况下,热点方法会经过 3 层的 C1 编译,然后再被 4 层的 C2 编译。

如果方法的字节码数目比较少(如 getter/setter),而且 3 层的 profiling 没有可收集的数据。那么 JVM 断定该方法对于 C1 代码和 C2 代码的执行效率相同。在这种情况下,Java 虚拟机会在 3 层编译之后,直接选择用 1 层的 C1 编译。由于这是一个终止状态,因此 Java 虚拟机不会继续用 4 层的 C2 编译。