当JVM的初始化完成后,类在调用执行过程中,执行引擎会把字节码转换成机器码,然后在操作系统中才能执行。在字节码转换为机器码的过程中,虚拟机中还存在着一道编译,那就是即时编译JIT。
最初,JVM中的字节码是由解释器(Interpreter)完成编译的,当虚拟机发现某个方法或代码块运行特别频繁时,就会把这些代码认定为热点代码。
为了提高热点代码的执行效率,在运行时,即时编译器(JIT, Just In Time)会把这些代码编译成与本地平台相关的机器码,并进行各层次的优化,然后保存到内存中。
怎么样才会被认为是热点代码呢?JVM中会设置一个阈值,当方法或者代码块的在一定时间内的调用次数超过这个阈值时就会被编译,存入codeCache中。当下次执行时,再遇到这段代码,就会从codeCache中读取机器码,直接执行,以此来提升程序运行的性能。整体的执行过程大致如下图所示:
C1编译器
C1编译器是一个简单快速的编译器,主要的关注点在于局部性的优化,适用于执行时间较短或者对启动性能有要求的程序,也称Client Compiler,例如,GUI应用对界面启动速度就有一定要求。
C2编译器
C2编译器是为长期运行的服务器端应用程序做性能调优的编译器,适用于执行时间较长或者对峰值性能有要求的程序,也称为Server Compiler,例如,服务器上长期运行的Java应用对稳定运行就有一定的要求。
Graal Compiler
从JDK 9开始,Hotspot VM中集成了一种新的Server Compiler,Graal编译器。JVM会在解释执行的时候收集程序运行的各种信息,然后编译器会根据这些信息进行一些基于预测的激进优化,比如分支预测,根据程序不同分支的运行概率,选择性地编译一些概率较大的分支。Graal比C2更加青睐这种优化,所以Graal的峰值性能通常要比C2更好。
Graal编译器可以通过Java虚拟机参数-XX:+UnlockExperimentalVMOptions -XX:+UseJVMCICompiler启用。当启用时,它将替换掉HotSpot中的C2编译器,并响应原本由C2负责的编译请求。
分层编译
在Java7之前,需要根据程序的特性来选择对应的JIT,虚拟机默认采用解释器和其中一个编译器配合工作。
Java7引入了分层编译,这种方式综合了C1的启动性能优势和C2的峰值性能优势,我们也可以通过参数-client或者-server强制指定虚拟机的即时编译模式。
分层编译将JVM的执行状态分为5个层次:
- 第0层:程序解释执行,默认开启性能监控功能(Profiling),如果不开启,可触发第二层编译;
- 第1层:可称为C1编译,将字节码编译为本地代码,进行简单、可靠的优化,不开启Profiling;
- 第2层:也称为C1编译,开启Profiling, 仅执行带方法调用次数和循环回边执行次数profiling的C1编译;
- 第3层:也称为C1编译,执行所有带Profiling的C1编译;
- 第4层:可称为C2编译,也是将字节码编译为本地代码,但是会启用一些编译耗时较长的优化,甚至会根据性能监控信息进行一些不可靠的激进优化。
profiling就是收集能够反映程序执行状态的数据。其中最基本的统计数据就是方法的调用次数,以及循环回边的执行次数。
通常情况下,C2代码的执行效率要比C1代码的高出30%以上。C1层执行的代码,按执行效率排序从高至低则是1层>2层>3层。这5个层次中,1层和4层都是终止状态,当一个方法到达终止状态后,只要编译后的代码并没有失效,那么JVM就不会再次发出该方法的编译请求的。服务实际运行时,JVM会根据服务运行情况,从解释执行开始,选择不同的编译路径,直到到达终止状态。
相关参数
-Xint:相当于-XX:-UseCompiler,以解释器Interpreter模式运行(关闭C1, C2)
-XX:TieredStopAtLevel=4:使用C1,C2编译器,并停留在第四层,这也是默认值
-XX:+TieredCompilation:开启分层编译,JDK8之后默认开启
-XX:-TieredCompilation:只开启 C2,关闭分层编译
-XX:-OmitStackTraceInFastThrow:关闭fast throw优化
-XX:InitialCodeCacheSize:codeCache初始大小
-XX:ReservedCodeCacheSize:codeCache最大大小,默认240MB
-XX:CompileThreshold:当方法的调用次数和循环回边的次数的和,超过由参数-XX:CompileThreshold指定的阈值时(使用C1时,默认值为1500;使用C2时,默认值为10000),就会触发即时编译
开启分层编译的情况下,-XX:CompileThreshold参数设置的阈值将会失效,触发编译会由以下的条件来判断:
- 方法调用次数大于由参数-XX:TierXInvocationThreshold指定的阈值乘以系数。
- 方法调用次数大于由参数-XX:TierXMINInvocationThreshold指定的阈值乘以系数,并且方法调用次数和循环回边次数之和大于由参数-XX:TierXCompileThreshold指定的阈值乘以系数时。
默认C1, C2编译器线程数由CPU核心数决定。
-XX:CICompilerCount=N: N/3是C1编译线程数,(N - N/3)是C2编译线程数,比如C1CompileCount=6,那么c1线程有2个,c2线程有4个。
-XX:+PrintCompilation: 输出应用代码编译细节,方便调优
JVM查看参数
-XX:+PrintFlagsInitial: 这个参数显示在处理参数之前所有可设置的参数及它们的值,然后直接退出程序
-XX:+PrintCommandLineFlags: 这个参数的作用是显示出VM初始化完毕后所有跟最初的默认值不同的参数及它们的值
-XX:+PrintFlagsFinal: 显示JVM初始化完后的参数值
通过java -XX:+PrintFlagsFinal -version查看默认信息。
参考:
OmitStackTraceInFastThrow与JVM的C1,C2编译器 - 掘金