vlambda博客
学习文章列表

Java编译器优化机制

1. 字节码是如何运行的

    Java有两种运行模式,分别是:

    • 解释执行:由解释器一行一行翻译执行;

    • 编译执行:把字节码编译成机器码,直接执行机器码

2. 解释执行VS编译执行

    解释执行:

      • 优势在于没有编译的等待时间

      • 由于一行一行的去翻译,性能相对就差一些

    编译执行:

      • 运行效率会高很多,一般认为比解释执行快一个数量级

      • 带来了额外的开销,比如说内存开销、CPU开销等

3. 查看Java运行模式

    使用“java -version”命令查看运行模式,如下所示:

    上图中可以看到运行模式mixed mode,mixed mode是混合模型,意思是部分代码解释执行,部分代码编译执行。

    -Xint:设置JVM的执行模式为解释执行模式

Java编译器优化机制

    如果想让springboot项目以解释模式执行,可以使用“java -Xint xxx.jar”就可以了。

    -Xcomp:JVM优先以编译模式执行,不能编译的,以解释模式执行

Java编译器优化机制

    -Xmixed:JVM以混合模式运行,默认就是该模式

Java编译器优化机制

    一般情况下Java代码一开始由解释器解释执行,当虚拟机发现某个方法或者某个代码块在频繁的运行的时候,就会认为这些代码是“热点代码”,为了提升热点代码的执行效率,会用即时编译器(也就是经常看到的JIT)把这些热点代码编译成与本地平台相关的机器码,并进行各层次的优化

4. HotSpot的即时编译器

    C1编译器:

      • 是一个简单快速的编译器

      • 主要关注局部性的变化

      • 适用于执行时间较短或对启动性能有要求的程序。例如:GUI应用对界面的启动就有一定的要求(IDEA)

      • 也被称为是Client Compiler

    C2编译器:

      • 是为长期运行的服务器端应用程序做性能调优的编译器(比如SpringBoot程序)

      • 适用于执行时间较长或对峰值性能有要求的程序

      • 也被称为Server Compiler

5. 分层编译

      • 0级别:解释执行

      • 1级别:简单C1编译,会用C1编译器做一些简单的优化,不开启Profiling(JVM的性能监控)

      • 2级别:受限的C1编译,仅执行带有方法调用次数以及循环回边执行次数Profiling的C1编译

      • 3级别:完全C1编译,会执行所有带有Profiling的C1代码

      • 4级别:使用C2编译器进行优化,该级别会启用一些编译时间较长的优化,一些情况下会根据性能监控信息进行一些非常激进的优化

    一般来说级别越高,应用启动越慢,优化的开销越高,峰值性能也越高。默认情况下,JDK8是开启分层编译的,如果只想开启C2,不想使用C1,可以使用下面参数禁止中间编译层(123层):

    只开启C2:-XX:-TieredCompilation

    只开启C1:-XX:+TieredCompilation -XX:TieredStopAtLevel=1

6. 如何找到热点代码

    目前来说业界找到热点代码的思路有以下两种:

      • 基于采样的热点探测:周期检查各个线程的栈顶,如果发现某些方法一直出现在栈顶,说明这个方法是热点方法

      • 基于计数器的热点探测:为每个方法或代码块建立计数器,统计执行的次数,如果超过一定的阈值,就说明是热点代码(HotSpot虚拟机使用的就是这种探测机制)

7. HotSpot内置的两类计数器

      • 方法调用计数器:用于统计方法被调用的次数,在不开启分层编译的情况下,在C1编译器下的默认阈值是1500次,在C2编译器下默认是1000次,可以使用-XX:CompileThreshold=x指定阈值

      • 回边计数器:用于统计一个方法中循环体代码执行的次数,在字节码中遇到控制流向后跳转的指令称为“回边”(Back Edge)。在不开启分层编译的情况下,在C1编译器下的默认阈值是13995次,在C2编译器下默认是10700次。可使用-XX:OnStackReplacePercentage=x指定阈值,建立回边计数器的目的主要是为了触发OSR(On-Stack Replacement)编译,参考文档为:

        https://www.zhihu.com/question/45910849/answer/100636125

    方法调用计数器流程:

    如果不做任何设置,方法调用计数器统计的并不是方法被调用的绝对次数,而是一个相对的执行频率,即一段时间之内方法被调用的次数。当超过一定的时间限度,如果方法的调用次数仍然不足以让它提交给即时编译器编译,那这个方法的调用计数器就会被减少一半,这个过程称为方法调用计数器热度的衰减,而这段时间就称为此方法统计的半衰周期。进行热度衰减的动作是在虚拟机进行垃圾收集时顺便进行的,可以使用虚拟机参数-XX:-UseCounterDecay来关闭热度衰减,让方法计数器统计方法调用的绝对次数,这样只要系统运行时间足够长,绝大部分方法都会被编译成本地代码。另外,可以使用-XX:CounterHalfLifeTime参数设置半衰周期的时间,单位是秒。

    回边计数器流程:

8. 总结

    本节主要介绍了编译器优化相关的解释运行、编译运行、JVM内置的两款即时编译器、分层编译、热点代码查找内容,本节的概念对于高级开发人员是必须要掌握的基础内容,对于涉及到的JVM参数用时查询即可,本节涉及到的JVM参数如下所示:

参数
作用
-Xmixed
混合模式运行(默认方式)
-Xint
设置JVM的执行模式为解释执行模式
-Xcomp
JVM优先以编译模式运行,不能编译的,以解释模式运行
-XX:-TieredCompilation
禁止中间层编译
-XX:TieredStopAtLevel 到哪个分层停止
-XX:CompileThreshold=X 指定方法调用计数器阈值(关闭分层编译时才有效)
-XX:OnStackReplacePercentage=X 指定回边计数器阈值(关闭分层编译时才有效
-XX:-UseCounterDecay 关闭方法调用计数器热度衰减
-XX:CounterHalfLifeTime 指定方法调用计数器半衰周期(秒)