|JVM底层原理之HotSpotVM的热点探测机制

|JVM底层原理之HotSpotVM的热点探测机制

JVM底层原理之HotSpotVM的热点探测机制
【|JVM底层原理之HotSpotVM的热点探测机制】HotSpot VM使用的是基于计数器的热点探测 , 但是它在此方法上 , 又分成了两个计数器 , 当一个方法的这两个计数器之和超过阈值 , 才会被认为是热点方法并发送编译请求 。


其实通过这个计数器的计数对象 , 我们可以明白HotSpotVM的热点代码是以“方法”为单位的 。
①方法调用计数器:默认是统计一段时间内方法被调用的次数 , 这也是标准的计数方式 ,
非分层编译时 , 阈值的默认值在C1(client)模式下是1500次 , 在C2(server)模式下是10000次 。 可通过-XX: CompileThreshold来设定这个值 。 但在分层编译开启时 , 这个阈值设定会被忽略 , 因为这时会根据当前待编译的方法数以及编译线程数来动态调整阈值 。
阈值频率和半衰周期默认设置下 , 方法调用计数器不是统计调用的绝对次数 , 而是执行频率:是一段时间内方法被调用的次数 。 而当一段时间内仍然达不到阈值 , 无法触发编译请求 。 计数器就会被减少一半 , 这个过程被称为方法调用计数器的热度衰减 , 而这段时间就称为方法调用计数器的半衰周期 , 进行热度衰减的动作是在JVM进行垃圾回集时顺便进行的 。
正是因为这个热度衰减 , 才让方法有选择的被筛选为热点代码 , 而不是全部都被编译 。 有需要的话也可以通过参数自己设置半衰期的时间长度(单位是秒) , 甚至关闭这个热度衰减 。 如果关闭了热度衰减的话 , 只要程序运行的时间够久 , 所有方法都有被编译的可能(因为计数器只增不减) , 但是这也显然会给系统增添不小负担 , 这点要自己深入了解原理后自己权衡利弊 。
②回边计数器:主要是统计方法内循环体代码执行的次数 , 在字节码遇到控制流后向后跳转的指令被称为回边(Back Edge) , 注意它统计的是回边次数 , 不是循环次数 。 如果是个空循环 , 它每次遇到控制流后都继续跳转到自己 , 这就不是回边了 , 也不会被统计 。
因为有些代码虽然被调用的次数没那么多 , 但是它执行一次就要循环个几十上百次的 , 这不就是很明显的热点代码了吗?所以通过结合回边计数器 , 可以更准确的探测到需要被编译的热点代码 。
它的阈值是通过一段公式计算得来的 , 非分层编译时 , C1 默认为 13995 , C2 默认为 10700 , 但是我们也可以通过调整OSR比率-XX:OnStackReplacePercentage来间接的调整阈值:
C1模式的回边计数器阈值计算公式:CompileThreshold*OnStackReplacePercentage/100;即:【方法调用计数器的阈值】乘以【OSR比率】/100;

C2模式的回边计数器阈值计算公式:(CompileThreshold)*(OnStackReplacePercentage-InterpreterProfilePercentage)/100;即:【方法调用计数器的阈值】乘以(【OSR比率】减去【解释器监控比率】)/100;

当然 , 在分层编译下 , 这个配置就无效了 , 因为这时会根据当前待编译的方法数以及编译线程数来动态调整阈值 。
而且这个计数器并没有热度衰减 , 因此它统计的就是该方法执行循环的绝对次数 。
OSR运行时替换栈帧技术建立回边计数器另外的功能也是为了触发OSR(On-Stack Replacement , 运行时替换栈帧技术) , 又名栈上替换或栈帧替换 。 在一些循环周期比较长的代码段中 , 例如它循环了1千次1万次甚至更多 。 当回边次数达到冲破回边计数器阈值时 , JVM就有所动作了 。 首先它会认为这段是热点代码 , 然后让JIT将这个方法的循环体或从这个循环开始的代码进行编译 。 按照以往的逻辑 , 我们要等到至少下次调用这个方法时 , 才能将编译的代码派上用场 , 然后只能干巴巴的等着这个超长循环无止境的执行下去 。。。 直到它执行完当前的方法 。。 这就明显有点问题了 , 万一它就只调用这么一次呢?