即时编译器
当虚拟机发现某个方法或代码块的运行特别频繁时,会把这些代码认定为热点代码,为了提高热点代码的执行效率,在运行时虚拟机会将这些代码编译成与本地平台无关的机器码,并进行各种层次的优化,完成这个任务的编译器称为即时编译器(Just In Time Compiler,简称JIT编译器)
在HotSpot中采用了解释器和编译器并存的架构,称为混合模式
先来介绍一下解释器和编译器
解释器和编译器
这个就要说一下什么是编译型语言?什么是解释型语言?
C++这种语言被称为编译型语言,因为它们的程序都以编译后的二进制形式交付先写程序,然后编译器静态生成二进制文件。
PHP这种语言是解释型语言,只要机器上有合适的解释器,相同的程序可以在任何CPU上运行,执行程序时,解释器会将相应的代码转换为二进制代码。其优点是程序可移植,相同的代码放到有适当解释器的机器上,就可以运行。其缺点就是运行起来会很慢,因为每次执行都会重新翻译一遍每一行代码
java想要将两种方式中合起来,所以提供了一个虚拟机,将java应用编译之后(此时编译出来的不是CPU所能识别的二进制代码,而是虚拟机可识别的汇编语言,也就是java字节码),然后java字节码可以使用java运行(这里就相当于php解释php脚本了),使得java成为一门平台独立的解释型语言,由于java程序运行时的是java字节码,这个是有规范的,所以可以在代码执行时将其编译成平台特定的二进制代码,这个编译的过程是在程序执行时进行的,所以被称为即时编译(JIT)
使用java -version可以看到
1 | java -version |
如果想要强制使用解释模式,使用参数-Xint
1 | java -Xint -version |
如果想要强制使用编译模式,使用参数-Xcomp
1 | java -Xcomp -version |
热点探测
JVM执行代码时,并不会立即编译代码,原因是
- 如果代码只执行一次,那编译完全就是浪费,对于只编译一次的代码,解释执行java字节码比先编译在执行的速度快
- 多次执行来使得JVM更加的了解这段代码,使得JVM在编译时可以进行优化
判断一段代码是不是热点代码,是不是需要触发即时编译,这种行为称为热点探测,主要的热点探测判定方式有两种
基于采样的热点探测:该方法虚拟机会周期性的检查各个线程的栈顶,如果发现某个或某些方法经常出现在栈顶,那这个方法就是热点方法(J9采用的是这种方式)
优点:实现简单、高效,可以很容易的获取方法调用关系
缺点:很难精确地确认一个方法的热度,容易受到线程阻塞或别的因素的影响而扰乱热点探测
基于计数器的热点探测:该方法虚拟机会为每个方法建立计数器,统计方法的执行次数,如果超过阈值则是热点方法(HotSpot采用的该种方式),阈值是在Client模式下是1500次,在Server模式下是10000次,可以通过
-XX:CompileThreshold
设定优点:统计结果更加精确和严谨
缺点:实现麻烦,需要为每个方法建立并维护计数器,而且不能直接获取到方法的调用关系
当一个方法被调用时,会先检查该方法是否存在被JIT编译过的版本,如果存在,则优先使用编译后的本地代码来执行;如果不存在已被编译过的版本,则将此方法的调用计数器值加1,然后判断方法调用计数器与回边计数器之和是否超过方法调用计数器的阈值,如果已超过阈值,那么将会向即时编译器提交一个该方法的代码编译请求
参数配置
-XX:-UseCounterDecay
关闭热度衰减,默认是开启热度衰减,超过一段时间限度,如果方法的调用次数仍然不足以让它提交给即时编译器编译,那么这个方法的调用计数器就会被减少一半,该方式称为方法调用计数器的热度衰减,这个时间限制称为半衰周期。如果关闭热度衰减,那么只要系统运行时间足够长,绝大部分方法都会被即时编译器编译-XX:CounterHalfLifeTime
设置半衰周期时间,单位秒-XX:CompileThreshold
方法调用计数器阈值-XX:BackEdgeThreshold
回边计数器阈值,并未使用该参数,而是使用的OSR比率来进行计算-XX:OnStackReplacePercentage
OSR比率,回边计数器阈值=方法调用计数器阈值*OSR比率/100