热点代码探测确定何时JIT
是否需要启动JIT编译器将字节码直接编译为对应平台的机器码需要根据代码被调用执行的频率而定。那些需要被编译为本地代码的字节码也被称为“热点代码”,JIT编译器在运行时会针对那些频繁被调用的“热点代码”做出深度优化,将其直接编译为对应平台的机器码,以此提升Java程序的执行性能。一个被多次调用的方法,或者是一个方法体内部循环次数较多的循环体都可以被称为“热点代码”,因此都可以通过JIT编译器编译为机器码,并缓存起来。一个方法被多次调用的时候,从解释执行切换到编译执行是在两次方法调用之间产生的,因为上一次方法在被调用的时候还没有将该方法编译好,所以仍然需要继续解释执行,而不需要去等待程序被编译,否则太浪费时间了,等再次调用该方法的时候,发现该方法已经被编译好,那么就会使用编译好的机器码执行了。
main()方法被执行的次数只有一次,但是方法体内部有一个循环20000次的循环体,这种情况下,就需要将循环体编译为机器码,而不是将main()方法编译为机器码,这个时候就需要在循环入口处判断是否该循环体已经被编译为机器码。由于这种编译方式不需要等待方法的执行结束,因此也被称为栈上替换编译,或简称OSR(On Stack Replacement)编译。一个方法究竟要被调用多少次,或者一个循环体究竟需要执行多少次循环才可以达到这个标准?必然需要一个明确的阈值,JIT编译器才会将这些“热点代码”编译为机器码执行。这里主要依靠热点探测功能,比如上面代码的循环次数为20000次,那么就可能在循环执行5000次的时候开始被编译,然后在第5200次循环的时候开始使用机器码,中间的20次循环依然是解释执行,因为编译也是需要消耗时间的。目前HotSpot VM所采用的热点探测方式是基于计数器的热点探测。HotSpot VM会为每一个方法都建立两个不同类型的计数器,分别为方法调用计数器(Invocation Counter)和回边计数器(Back Edge Counter)。方法调用计数器用于统计方法的调用次数,回边计数器则用于统计循环体执行的循环次数。方法调用计数器的默认阈值在Client模式下是1500次,在Server模式下是10000次。超过这个阈值,就会触发JIT编译。这个阈值可以通过虚拟机参数-XX:CompileThreshold来手动设定。一般而言,如果以缺省参数启动Java程序,方法调用计数器统计的是一段时间之内方法被调用的次数。当超过一定的时间限度,如果方法的调用次数没有达到方法调用计数器的阈值,这个方法的调用计数器的数值调整为当前数值的1/2,比如10分钟之内方法调用计数器数值为1000,下次执行该方法的时候,方法调用计数器的数值从500开始计数。这个过程称为方法调用计数器热度的衰减(Counter Decay),而这段时间就称为此方法统计的半衰周期(Counter Half Life Time),可以使用-XX:CounterHalfLifeTime参数设置半衰周期的时间,单位是秒。可以使用JVM参数“-XX:-UseCounterDecay”关闭热度衰减,让方法计数器统计方法调用的绝对次数,这样,只要系统运行时间足够长,绝大部分方法都会被编译成机器码。一般而言,如果项目规模不大,并且产品上线后很长一段时间不需要进行版本迭代,都可以尝试把热度衰减关闭,这样可以使Java程序在线上运行的时间越久,执行性能会更佳。