java 程序通过解释器进行解释执行,如果发现热点代码(多次调用的方法或者多次执行的循环体),则会使用即时编译器将这些部分编译为与本地相关的机器码,并进行各种优化。
许多主流商用虚拟机都包括解释器与编译器,解释器有助于启动时节省编译时间,立即执行。编译器在程序启动后,不断将代码编译为本地代码,提高执行效率。解释器还可以在编译器进行激进的优化尝试出现问题时,作为“逃生门”,通过逆优化退回解释状态继续执行。
对于由循环体触发的编译操作,编译器依然将整个方法作为编译对象。这种方式发生在方法执行过程中,所以又被称为(栈上替换)
方式名称 | 判断细节 |
基于采样的热点探测 | 虚拟机周期性检查各个线程栈顶,将经常出现在栈顶的方法判定为热点方法。简单高效而且容易获得调用关系,但由于受外界因素影响而很难精准地确认方法的热度。 |
基于计数器的热点探测 | 虚拟机为每个方法(或代码块)建立计数器,统计执行次数,超过阈值则判定为热点方法。并不能获得调用关系,但是结果更加精确严谨 |
HotSpot 中设置了两个即时编译器,Client Compiler 和 Server Compiler(简称C1和C2)。一般采用解释器与其中一个编译器直接配合的方式工作,称为“混合模式”。用户也可以自行设置“解释模式”(编译器不参与工作)或者“编译模式”(优先采用编译方式执行,但是在无法编译时解释器会介入),而由于编译本地代码需要占用程序运行时间,而且解释器要为编译器收集性能监控器信息,所以对执行速度会有影响。为了达到平衡,HotSpot 启用分层编译的策略。
层次 | 具体操作 |
0 | 解释执行,解释器不开启性能监控功能,可触发第1层编译 |
1 | C1编译,将字节码编译为本地代码,进行简单可靠的优化。必要时加入性能监控逻辑 |
2 | C2编译,与C1不同的是,会启用编译耗时较长的优化,甚至是进行不可靠的激进优化 |
实施分层编译后,Client Compiler 与 Server Compiler 将同时工作,许多代码可能会被多次编译。Client Compiler 获得更高编译速度,Server Compiler 获得更高编译质量,解释执行时也不再收集性能监控信息。
HotSpot 采用的是计数器的方式,并准备了两种计数器:方法调用计数器与回边计数器
调用计数器:
阈值:Client 模式下1500,Server 模式下10000
方法被调用时,先查看是否存在被编译过的版本,存在则优先使用编译过的本地代码。不存在则将计数器值+1,然后判断调用计数器与回边计数器的和是否超过调用计数器的阈值。超过阈值请求即时编译器执行编译。如果不进行任何设置,执行引擎将继续进入解释器解释字节码直到提交的请求被编译完成。然后方法的调用入口地址会被改为新的,下一次调用时会使用已编译的版本。
不进行任何设置时,调用计数器并不是统计绝对次数,一段时间内相对的执行频率。超过一定时间但调用次数仍不足以达到提交给即时编译器的要求时,则该方法的调用计数器减半(计数器热度的衰减),该时间也被称为此方法统计的半衰周期。衰减动作在回收垃圾时顺便执行。
回边计数器:
作用是统计方法体中控制流向后跳转的指令出现次数,也就是“回边”出现次数(空循环不会被统计)。该计数器目的在于触发OSR编译。
Client 模式下,阈值计算公式为:方法调用计数器阈值*OSR比率(默认933)/100,全部采取默认值时,结果为13995
Server 模式下,阈值计算公式为:方法调用计数器阈值*OSR比率(默认933)-解释器监控比率/100,全部采取默认值时,结果为10700
整体过程与调用计数器相似,不同在于超过阈值时会发出 OSR请求,并降低计数器的值以便继续在解释器中执行循环。而且没有计数热度衰减的过程,统计的就是循环执行的绝对次数。当计数器溢出时,计数器的值也会被调整到溢出状态,下次进入方法就执行标准编译过程。
默认情况下,虚拟机在编译器未完成之前都将按照解释的方式执行,编译动作则在后台线程进行。