Java强制虚拟机运行于“解释器模式”的探测方式优化

文章目录

一、解释器和编译器

Java程序运行时,主要执行字节码指令。通常,这些指令由解释器(Interpreter)解释和执行。这是解释执行。

当虚拟机发现某个方法或代码块运行非常频繁时,会将这些代码识别为热代码。为了提高热代码的执行效率,虚拟机会在运行时将这些代码编译成与本地平台相关的机器码,并进行多层次的优化。完成这个任务的编译器称为即时编译器(Just In Time Compiler,简称JIT编译器)。

图片[1]-Java强制虚拟机运行于“解释器模式”的探测方式优化-老王博客

HotSpot虚拟机内置了两个实时编译器,分别称为Client Compiler和Server Compiler,简称C1编译器和C2编译器。

不管使用的编译器是C1还是C2,解释器配合编译的方式在虚拟机中被称为“混合模式”。

图片[2]-Java强制虚拟机运行于“解释器模式”的探测方式优化-老王博客

可以通过参数-Xint强制虚拟机运行在“解释器模式”,这里编译器根本不工作,所有代码都以解释器模式执行。

您还可以使用参数 -Xcomp 强制虚拟机以“编译器模式”运行。在这种情况下,会先使用编译模式来执行程序,但是当编译无法进行时,解释器仍然需要介入执行过程。

二、热点代码和热点检测

热点代码是调用频繁的代码,会被编译缓存以备下次使用,但对于那些执行次数很少的代码,这种编译动作纯属浪费。

JVM提供了一个参数-XX:ReservedCodeCacheSize来限制CodeCache的大小,JIT编译后的代码会放在CodeCache中。JDK7的默认值为32m~48m推荐c语言手机编译器,JDK8的默认值为240m。

如果这个空间不够,JIT就无法继续编译,编译执行会变成解释执行,性能会降低。同时,JIT 编译器会一直尝试优化代码,导致 CPU 使用率增加。

判断一段代码是否为热点代码,是否需要启动实时编译,称为热点检测。热点检测方法主要有两种,如下:

基于采样的热点检测:使用该方法的虚拟机周期性地检查每个线程的栈顶,如果发现某个方法(或某些)频繁出现在栈顶,则该方法为“热点方法” ”。

缺点:基于采样的热点检测的优点是实现简单高效,还可以方便的获取方法调用关系(只需展开调用栈)。其他外部因素的影响会干扰热点检测。

基于计数器的热点检测:使用该方法的虚拟机为每个方法(甚至是一个代码块)建立一个计数器,统计方法的执行次数,如果执行次数超过一定阈值,则认为是“热点方法”。

缺点:这种统计方法实现起来比较麻烦。它需要为每个方法建立和维护一个计数器,不能直接得到方法的调用关系,但是它的统计结果相对来说更加准确和严谨。

HotSpot虚拟机基于第二种——基于计数器的热点检测,为每个方法准备了两种类型的计数器:方法调用计数器和后端计数器

2.1、方法调用计数器

它用于计算方法被调用的次数。其默认阈值在 Client 模式下为 1500 次,在 Server 模式下为 10000 次。这个阈值可以通过虚拟机参数-XX:CompileThreshold手动设置。

当一个方法被调用时,它会首先检查该方法是否有 JIT 编译的版本。如果存在,将首先使用已编译的本机代码。如果没有编译版本,就会调用这个方法。计数器加1,然后判断方法调用计数器和后端调用计数器之和是否超过方法调用计数器的阈值。如果超过阈值推荐c语言手机编译器,将向即时编译器提交该方法的代码编译请求。

如果没有设置,则方法调用计数器计算的不是方法调用的绝对次数,而是相对执行频率和一段时间内方法被调用的次数。当超过一定的时间限制时,如果方法的调用次数仍然不足以提交给即时编译器进行编译,则该方法的调用计数器将减半,这个过程称为方法调用计数器热衰减。

对于上面提到的方法调用计数器热衰减,我们也可以通过虚拟机参数-XX:-UseCounterDecay关闭热衰减,让方法计数器统计方法调用的绝对次数,这样只要系统运行时间够长,大部分代码都会被编译器编译为本机代码。或者,您可以使用 -XX:CounterHalfLifeTime 参数以秒为单位设置半衰期。

2.2、后沿计数器

它的作用是统计一个方法中的循环体代码被执行的次数。在字节码中遇到控制流向后跳转的指令称为“后沿”。与方法调用计数器不同,环回计数器不计算热衰减过程,因此该计数器计算方法循环执行的绝对次数。

三、分层编译

基于上述C1和C2编译器的优缺点,虚拟机一般启动分层编译的策略(开启分层编译参数:-XX:+TieredCompilation),分层编译根据编译器。规模和耗时,分为不同的编译级别,包括:

在第0层,程序被解释执行,解释器启动性能监控功能,可以触发第1层编译。第 1 层,也称为 C1 编译,将字节码编译为本机代码,执行简单、可靠的优化,并在必要时添加性能监控逻辑。Layer 2 也称为 C2 编译,也将字节码编译为原生代码,但会启动一些编译时间较长的优化,甚至是一些基于性能监控信息的不可靠的激进优化。

实现分层编译后,C1和C2会同时工作,很多代码可能会被多次编译,使用C1编译器编译速度更快,使用C2编译器编译质量更高。

四、编译优化4.1、方法内联

方法内联的优化行为是将目标方法的代码复制到发起调用的方法中,避免实际的方法调用。

图片[3]-Java强制虚拟机运行于“解释器模式”的探测方式优化-老王博客

JVM 自动识别热点方法并使用方法内联对其进行优化。但是,热点方法不一定由 JVM 内联。例如,如果方法体太大,JVM 就不会执行内联操作。默认情况下,小于 325 字节的方法体将被内联。我们可以通过 -XX:FreqInlineSize =N 来设置大小值。

4.2、标量替换

逃逸分析证明一个对象不会被外部访问。如果可以拆分对象,则程序实际执行时可能不会创建对象,而是创建其成员变量。

图片[4]-Java强制虚拟机运行于“解释器模式”的探测方式优化-老王博客

对象拆分后,对象的成员变量可以分配在栈上或寄存器上,原对象不需要分配内存空间。这种编译优化称为标量替换(前提是需要开启逃逸分析)。

© 版权声明
THE END
喜欢就支持一下吧
点赞0
分享
评论 抢沙发

请登录后发表评论