程序编译与代码优化 - 晚期(运行期)优化

1. 概述

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

即时编译器并不是虚拟机必须的部分,Java虚拟机规范并没有规定Java虚拟机内必须要有即时编译器存在,更没有限定或指导即时编译器应该如何去实现。但是,即时编译器编译性能的好坏、代码优化程度的高低却是衡量一款商用虚拟机优秀与否的最关键的指标之一,它也是虚拟机中最核心且最能体现虚拟机技术水平的部分。

 由于Java虚拟机规范没有具体的约束规则去限制即时编译器应该如何实现,所以这部分功能完全是与虚拟机具体实现相关的内容。本章提及的编译器、即时编译器都是指HotSpot虚拟机内的即时编译器,虚拟机也是特指HotSpot虚拟机。

2. HotSpot虚拟机内的即时编译器

2.1 解释器与编译器

许多主流的商用虚拟机,如HotSpot,J9等,都同时包含解释器与编译器。

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

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

无论采用的编译器是Client Compiler还是Server Compiler,解释器与编译器搭配使用的方式在虚拟机中称为“混合模式”(Mixed Mode),用户可以使用参数-Xint强制虚拟机运行于解释模式(Interpreted Mode),这是编译器完全不介入工作,全部代码都使用解释方式执行。也可以使用参数-Xcomp强制迅疾运行于编译模式(Compiled Mode,已废弃)。

可以通过虚拟机的-version命令的输出结果显示出3种模式(Mixed Mode,Interpreted Mode,Compiled Mode):

由于即时编译器编译本地代码需要占用程序运行时间,要编译出优化程度更高的代码,所花费的时间可能更长;而且想要编译出优化程度更高的代码,解释器可能还要替编译器收集性能监控信息,这对解释执行的速度也有影响。为了在程序启动响应速度与运行效率之间达到最佳平衡,HotSpot虚拟机还会逐渐启动分层编译(Tiered Compilation)的策略。

分层编译根据编译器编译、优化的规模与耗时,划分出不同的编译层次,其中包括:

  • 第0层,程序解释执行,解释器不开启性能监控功能,可触发第1层编译;
  • 第1层,也称为C1编译,将字节码编译为本地代码,进行监督、可靠的优化,如有必要将加入性能监控的逻辑;
  • 第2层(或2层以上),也称为C2编译,也是讲字节码编译为本地代码,但是会启用一些编译耗时较长的优化,甚至会根据性能监控信息进行一些不可靠的激进优化。

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

2.2 编译对象和触发条件

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

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

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

判断一段代码是不是热点代码,是不是需要出发即时编译,这样的行为称为热点探测(Hot Spot Detection),其实进行热点探测并不一定要知道方法具体被调用多少次,目前主要的热点探测判定方式有两种,分别如下:

  • 基于采样的热点探测(Sample Based Hot Spot Detection)
  • 基于计数器的热点探测(Counter Based Hot Spot Detection)

在HotSpot中使用了基于计数器的热点探测方法,因此为每个方法准备两类计数器:方法调用计数器和回边计数器。在确定虚拟机运行参数的前提下,这两个计数器都有一个确定的阈值,当计数器超过阈值,就会触发JIT编译。

2.3 编译过程

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

后台执行编译的过程中,编译器做了什么事情呢?

Server Compiler和Client Compiler两个编译器的编译过程是不一样的。

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

  • 第一个阶段,一个平台独立的前端将字节码构造成一种高级中间代码表示(HIR),在此之前编译器会在字节码上完成一部分基础优化,如方法内联、常量传播等优化将会在字节码被构造成HIR之前完成
  • 第二个阶段,一个平台相关的后端从高级中间代码表示(HIR)中产生低级中间代码表示(LIR),在此之前会在HIR上完成另外一些优化,如空值检查消除、范围检查消除等。
  • 第三个阶段,平台相关的后端使用线下扫描算法,在LIR上分配寄存器,并在LIR上做窥空优化,然后产生机器代码

流程如下图:

猜你喜欢

转载自www.cnblogs.com/lujiango/p/9154843.html