前言
通过前面我们知道,对于每个方法,HotSpot都维护两个计数器
- Invocation Counter:方法被调用次数,每被调用一次都会+1
- BackEdge Counter:专业的说法就是字节码在执行时的回跳次数。通俗点说就是,在For或者While循环中,每执行一次,都会+1。
并且我们知道对于一个方法,JIT有两种不同的编译方式
- 完整的原方法编译,就是把原本的方法逻辑进行编译。入参和运行结果和解释运行都是一致的。
- OSR编译,OSR后的方法入参以及运行流程和原方法有较大差异。
很自然得我们就会想到,其实计数器和编译方式之间是有对应关系的。
- Invocation Counter -> 完整的原方法编译
- BackEdge Counter -> OSR编译
当对应的方法计数器达到一定的次数,就会触发响应的编译
编译流程图
完整的编译流程如下:
注:该图引自R大的JVM分享PPT,如有侵权,请联系我删除
图中的流程非常的清晰,这里提几个小点:
问:对于同一方法,是否两种编译方式都可能会执行?
答:是的,而且两种代码可能同时被运行,但是正常情况下,只要运行的够久,都会运行完整的原方法编译后的代码。
问:具体哪种编译方式先触发?
答:其实无法确定,看哪个计数器先达到阈值
触发阈值
可能你还想更直观的了解下两个计数器的触发阈值到底是多少。
在HotSpot源码中,有这样两个参数:
intx CompileThreshold = 10000
globals.hpp > ”number of interpreted method invocations before (re-)compiling”
intx BackEdgeThreshold = 100000
globals.hpp > “Interpreter Back edge threshold at which an OSR compilation is invoked”
数值可能根据不同的发行版本略有不同,上面的数值是JDK7版本中的。
那么你可能以为
- 当Invocation Counter > Compile Threshold时,就会触发原来方法的JIT
- 当BackEdge Counter > BackEdge Threshold时,就会触发方法的OSR编译
但是事实并不是如此。
问题出在哪儿呢?难道官方的定义还会有错吗?
是的,问题出在BackEdgeThreshold上,虽然HotSpot中确实定义了该参数,描述中似乎也证实了该参数的作用,但是这个参数并没有实际使用过。
对于BackEdgeThreshold的计算,是另外一套公示。
1 | if (ProfileInterpreter) { |
首先解释下ProfileInterpreter
参数,这个参数也是在HotSpot中定义的,之前在文章HotSpot原理指南-C1和C2介绍
中也讲解过,就是是否在运行时收集方法的Profile信息,这个字段在Server模式默认是开启的。
所以大部分情况下,除非你的计算机比较老,都会根据第一个公示进行计算
(CompileThreshold * (OnStackReplacePercentage - InterpreterProfilePercentage)) / 100
其中OnStackReplacePercentage默认值是140,InterpreterProfilePercentage默认值是33。
由此我们可以计算出真实的BackEdge Invocation阈值大概是10700左右。
衰减
假如方法计数器不会根据时间进行衰减的话,那么只要服务器运行的时间足够长,再罕见被调用的函数,也会触发到阈值,然后被JIT编译。
这显然是不合理的,因为我们知道JIT后的机器码数据,还是会保存在内存中的,这样相当于一段逻辑在内存中又保存了字节码,又保存了一份机器码,十分的浪费内存。
所以对于Invocation Counter而言,经过一段时间,个数就会进行减少。
具体的减少逻辑,读者有兴趣的可以自己去探索。
但是注意:对于BackEdge Counter,是不会作衰减的。