即时编译器
即时编译器
当虚拟机发现某个方法或代码块的运行特别频繁,就会把这些代码认定为
“热点代码”(Hot Spot Code
),为了提高热点代码的执行效率,在运行时,
虚拟机将会把这些代码编译成本地机器码,并以各种手段尽可能地进行代码优化,
运行时完成这个任务的后端编译器被称为即时编译器。
解释器与编译器
并不是所有Java
虚拟机都采用解释器与编译器并存的运行架构,但主流的商用虚拟机
如HotSpot
、OpenJ9
等内部均包含了解释器与编译器。
当程序需要迅速启动和执行的时候,解释器可以首先发挥作用省去编译的时间,立即运行。
当程序启动后,随着时间的推移,编译器逐渐发挥作用,把越来越多的代码编译成本地代码,
可以减少解释器的中间损耗,获得更高的执行效率。
使用解释器节约内存
使用编译器提升效率
解释器还可以作为编译器激进优化时后备的“逃生门”。
HotSpot 的即时编译器
HotSpot
虚拟机内置了两个(或三个)即时编译器,其中有两个编译器存在已久,分别称为
“客户端编译器”(Client Compiler
)和“服务端编译器”(Server Compiler
),或者简称为
C1
编译器和C2
编译器(部分资料也称为Opto
编译器),第三个是JDK10
才出现的、长期
目标是代替C2
的Graal
编译器。
编译器的选择
在分层编译(Tiered Compilation
)的工作模式出现以前,HotSpot
虚拟机通常是采用解释器与
其中一个编译器直接搭配的方式工作,程序使用哪个编译器取决于虚拟机运行的模式。HotSpot
虚拟机会根据自身版本与宿主机器的硬件性能自动选择运行模式,用户可以使用-client
或-server
参数去强制指定虚拟机的运行模式。
无论使用的编译器是客户端编译器还是服务端编译器,解释器和编译器搭配使用的方式在虚拟机中
被称为“混合模式”(Mixed Mode
),用户也可以使用参数-Xint
强制虚拟机运行于“解释模式”(Interpreted Mode
),
这时候编译器完全不介入工作,全部代码都解释执行。也可以使用参数-Xcomp
强制虚拟机运行于“编译模式”(Compiled Mode
),
这时将优先采用编译方式执行程序,但是解释器仍然要在编译无法进行的情况下介入执行过程。
可以通过虚拟机的-version
命令输出这三种模式
java -version
java -Xint -version
java -Xcomp -version
分层编译
即时编译器编译本地代码需要占用程序运行时间,通常要编译出优化程度越高的代码,
所花费的时间便会越长。而且要编译出优化程度更高的带啊名,解释器可能还要替编译器
收集性能监控信息,这对解释器执行阶段的速度也有所影响。为了在程序启动响应速度与
运行效率之间达到最佳平衡,HotSpot
虚拟机在编译子系统中加入了分层编译的功能。
(JDK6
出现,JDK7
的服务端模式的虚拟机中默认开启)
第0层:程序纯解释执行,并且解释器不开启性能监控功能(
Profiling
)第1层:使用客户端编译器,进行简单可靠的稳定优化,不开启性能监控功能
第2层:使用客户端编译器,开启方法及回边次数统计等有限性能监控功能
第3层:使用客户端编译器,开启全部性能监控功能,出第2层的内容,还会
收集分支跳转、虚方法调用版本等统计信息
第4层:使用服务端编译器,会启动更多编译耗时更长的优化,且会根据监控信息进行
一些不可靠的激进优化
实施分层编译后,解释器、客户端编译器和服务端编译器会同时工作。热点代码可能会被
多次编译,用客户端编译器获取更高的编译速度,用服务端编译器获取更好的编译质量。
编译对象与触发条件
即时编译器编译的目标是“热点代码”,这里的热点代码主要有两类
- 被多次调用的方法
以整个方法作为编译对象,这种编译也是虚拟机中标准的即时编译方式
- 被多次执行的循环体
这种是为了解决一个方法只被调用过一次或少量的几次,但方法内部存在
循环此处较多的循环体。
尽管编译动作是由循环体所触发的,热点知识方法的一部分,但编译器依然
需要以整个方法作为编译对象,只是执行入口(从方法第几条字节码开始执行)
会有不同,编译时会传入执行入口点字节码序号(
Byte Code Index
,BCI
)。这种编译方式因为编译发生在方法执行的过程中,因此被形象地称为“栈上替换”
(
On Stack Replacement
,OSR
),即方法的栈桢还在栈上,方法就被替换了。
要知道某段代码是不是热点代码,是不是需要触发即时编译,这个行为称为“热点探测”
(Hot Spot Code Detection
),目前流行的热点探测判定方式有两种
- 基于采样的热点探测(
Sample Based Hot Spot Code Detection
)
采用这种做法的虚拟机会周期性地检查各个线程的调用栈顶,如发现某个方法
经常出现在栈顶,这个方法就是“热点方法”
简单高效,但很精准地确认一个方法的热度,容易受线程阻塞或别的外部因素影响。
- 基于计数器的热点探测(
Counter Based Hot Spot Code Detection
)
会为每个方法(甚至代码块)建立计数器,统计方法的执行次数,如果超过一定
阀值就认为它是热点方法。
相对麻烦,但统计结果更加准确严谨。
HotSpot 的热点探测
HotSpot
采用的是基于计数器的热点探测,为了实现热点探测提供了两类计数器
- 方法调用计数器(
Invocation Counter
) - 回边计数器(
Back Edge Counter
,回边的意思就是指在循环边界往回跳转)。
当虚拟机运行参数确定的前提下,这两个计数器都有一个明确的阀值,计数器阀值
一旦溢出,就会触发即时编译。
方法调用计数器
默认阀值在客户端模式下是1500次,在服务端模式下是10000次,通过-XX:CompileThreshold
设定。
当一个方法被调用时,虚拟机会先检测该方法是否存在被即时编译过的版本,如果存在则优先使用
编译后的本地代码来执行。否则该方法的调用计数器加一,然后判断方法调用计数器与回边计数器
值之和是否超过方法调用计数器的阀值,一旦超过将会向即时编译器提交一个该方法的代码编译请求。
方法调用计数器统计的不是绝对次数,而是一段时间内方法被调用的次数,当超过一定的时间限度,
如方法的调用次数仍然不足以让它提交给即时编译器编译,那该方法的调用计数会减少一半,称为
方法调用计数器的衰减(Counter Decay
),而这段时间被称为半衰周期(Counter Half Life Time
),
进行热度衰减的动作是进行垃圾收集时顺带进行的。可使用-XX:-UseCounterDecay
来关闭热度衰减,
让次数变为绝对次数,这样只要系统运行时间足够长,程序的绝大部分方法都会被编译为本地代码。
可使用-XX:CounterHalfLifeTime
设置半衰周期的时间,单位是秒。
回边计数器
字节码中遇到控制流向后跳转的指令就称为“回边”(Back Edge
),建立回边计数器统计的目的就是
为了触发栈上的替换编译。
通过-XX:OnStackReplacePercentage
间接调整回边计数器的阀值。
客户端模式下阀值 = (-XX:CompileThreshold
) * (-XX:OnStackReplacePercentage
) / 100,
-XX:OnStackReplacePercentage
默认值为933。如果都是默认值,则阀值为13995。
服务端模式下阀值 = (-XX:CompileThreshold
) *
((-XX:OnStackReplacePercentage
) - (-XX:InterpreterProfilePercentage
)) / 100
-XX:OnStackReplacePercentage
默认值为140,-XX:InterpreterProfilePercentage
默认值为33,
如果都是默认值,则阀值为10700。
当解释器遇到一条回边指令时,会先检查将要执行的代码片段是否有已经编译好的版本,
如果有将优先执行已编译的代码,否则回边计数器的值加一,然后判断方法调用计数器和回边
计数器值之和是否超过回边计数器的阀值,当超过阀值时将提交一个栈上替换编译请求,并把
回边计数器的值稍稍降低一些,以便继续在解释器中执行,等待编译器输出编译结果。
回边计数器没有计数器热度衰减的过程,统计次数就是绝对次数。当计数器溢出的时候,还会
把方法计数器的值也调整到溢出状态。
编译过程
在默认条件下,无论是方法调用产生的标准编译请求,还是栈上替换编译请求,虚拟机在编译器
还没完成编译之前都仍然将按照解释方法继续执行代码,而编译动作则在后台的编译线程中进行。
用户可以通过-XX:-BackgroundCompilation
来禁止后台编译,后台编译被禁止后,当触发即时编译时,
执行线程向虚拟机提交编译请求后将一直阻塞等待,直到编译过程完成再开始执行编译器输出的本地代码。