闲谈JVM(四):浅谈CodeCache与JIT

文章目录

  • 前言
  • JIT(just-in-time)
    • JIT参数配置
      • TieredCompilation
      • CompileThreshold
      • OnStackReplacePercentage
      • CICompilerCount
  • CodeCache
    • CodeCache参数配置
      • InitialCodeCacheSize
      • ReservedCodeCacheSize
      • Xmaxjitcodesize
      • CodeCacheMinimumFreeSpace
      • UseCodeCacheFlushing
  • JIT注意事项
  • CodeCache注意事项
  • 结语

前言

在前几篇中,

闲谈JVM(三):浅析本地元空间参数配置

闲谈JVM(二):浅析新老生代参数配置

闲谈JVM(一):浅析JVM Heap参数配置

我们对JVM最主要区域的常用参数的配置进行了介绍了解,涵盖了新生代、老生代、本地元空间(老年代)等区域,本篇,我们继续了解JVM中的另一个区域,JIT与CodeCache。

JIT(just-in-time)

在介绍CodeCache之前,我们先来了解一下Java的编译执行模式,在Java中提到编译,自然很容易想到 javac 编译器将 .java 文件编译成为 .class 文件的过程,这里的 javac 编译器称为前端编译器,其他的前端编译器还有诸如Intellij IDEA,JDT中的增量式编译器ECJ等

相对应的还有后端编译器,它在程序运行期间将字节码转变成机器码(现在的Java程序在运行时基本都是 解释执行加编译执行),如HotSpot虚拟机自带的JIT(Just In Time Compiler)编译器(分Client端和Server端)

Java程序最初是仅仅通过解释器解释执行的,即对字节码逐条解释执行,这种方式的执行速度相对会比较慢,尤其当某个方法或代码块运行的特别频繁时,这种方式的执行效率就显得很低。

于是后来在JVM中引入了JIT编译器(即时编译器),当虚拟机发现某个方法或代码块运行特别频繁时,达到某个阈值,就会把这些代码认定为Hot Spot Code(热点代码),为了提高热点代码的执行效率,在运行时,虚拟机将会把这些代码编译成与本地平台相关的机器码,并进行各层次的优化,完成这项任务的正是JIT编译器

JIT参数配置

关于JIT的相关参数主要有4个:TieredCompilation、CompileThreshold、OnStackReplacePercentage、CICompilerCount,我们来逐个分析。

TieredCompilation

TieredCompilation表示是否开启JVM的分层编译,该参数的使用方式如下:

1
2
-XX:+TieredCompilation(开启)
-XX:-TieredCompilation(关闭)

默认情况下,在JDK8以及以上的版本中,该参数默认开启,在JDK8之前,该参数是默认关闭的。

在JVM中存在三种代码编译执行模式,分别是解释执行、纯编译执行与分层编译执行,这几种方式有各自的特点。

1、解释执行
该模式下表示全部代码均是解释执行,不做任何JIT编译,这种模式会降低运行速度,通常低10倍或者更多,一般不会采用。

2、纯编译执行
这个参数可以使JVM运行在纯编译的模式,所有的方法在第一次调用的时候就会编成机器代码,但是现实的话,设置了这个参数之后系统启动负载的确没有上升,但是随之而来的问题,启动的时间会大幅度增加。

3、分层编译执行

HotSpot 内置两种编译器,分别是Client启动时的c1编译器和Server启动时的c2编译器:

c2在将代码编译成机器代码的时候需要搜集大量的统计信息以便在编译的时候进行优化,因此编译出来的代码执行效率比较高,代价是程序启动时间比较长,而且需要执行比较长的时间,才能达到最高性能;

与之相反,c1的目标是使程序尽快进入编译执行的阶段,所以在编译前需要搜集的信息比c2要少,编译速度因此提高很多,但是付出的代价是编译之后的代码执行效率比较低,但尽管如此,c1编译出来的代码在性能上比解释执行的性能已经有很大的提升;

所以所谓的分层编译,就是一种折中方式,在系统执行初期,执行频率比较高的代码先被c1编译器编译,以便尽快进入编译执行,然后随着时间的推移,执行频率较高的代码再被c2编译器编译,以达到最高的性能。

在此引用一下Oracle官方文档的解释:

Tiered compilation, introduced in Java SE 7, brings client startup speeds to the server VM. A server VM uses the interpreter to collect profiling information about methods that is fed into the compiler. In the tiered scheme, in addition to the interpreter, the client compiler generates compiled versions of methods that collect profiling information about themselves. Since the compiled code is substantially faster than the interpreter, the program executes with greater performance during this profiling phase. In many cases, a startup that is even faster than with the client VM can be achieved because the final code produced by the server compiler may be already available during the early stages of application initialization. The tiered scheme can also achieve better peak performance than a regular server VM because the faster profiling phase allows a longer period of profiling, which may yield better optimization. Use the -XX:+TieredCompilation flag with the java command to enable tiered compilation.

CompileThreshold

CompileThreshold表示触发JIT编译之前,解释执行一个方法的代码的执行次数的阈值(the number of method invocations needed before the method is compiled.),该参数的使用方式如下:

1
-XX:CompileThreshold=10000

默认情况下,在JDK8中,在Sever模式启动下,该参数的默认值为10000,在Client模式启动下,该参数为1500。

对于JVM来讲,如何去进行探测Hot Spot Code(热点代码),从而进行JIT的编译策略,就是通过这个阈值进行的判断。

当一个方法被调用时,会先检查该方法是否存在被JIT编译过的版本,如果存在,则优先使用编译后的本地代码来执行。

如果不存在已被编译过的版本,则将此方法的调用计数器值加1,然后判断方法调用计数器与回边计数器值之和是否超过方法调用计数器的阈值。如果超过阈值,那么将会向即时编译器提交一个该方法的代码编译请求。

如果不做任何设置,执行引擎并不会同步等待编译请求完成,而是继续进行解释器按照解释方式执行字节码,直到提交的请求被编译器编译完成。当编译工作完成之后,这个方法的调用入口地址就会自动改写成新的,下一次调用该方法时就会使用已编译的版本。

方法调用计数器触发JIT

OnStackReplacePercentage

OnStackReplacePercentage表示触发JIT编译之前,解释执行一个方法中循环体的代码的执行次数的阈值(the number of backwards branches taken in a method before it gets compiled),该参数的使用方式如下:

1
-XX:OnStackReplacePercentage=140

默认情况下,在JDK8中,在Sever模式启动下,该参数的默认值为140,在Client模式启动下,该参数为933。

针对循环体代码,JVM同样会进行热点代码的探测,识别是否可能存在热点代码,在JVM中,字节码中遇到控制流向后跳转的指令称为回边(BackEdge),JVM在方法中的循环体中添加回边计数器,执行次数超过阈值就认为是向编译器提交编译请求,触发OSR(On-Stack Replacement)编译。

对于OSR编译模式的解释,在此引用Oracle官方文档的说法:

This kind of compilation is called On-Stack Replacement (OSR), because even if the loop is compiled, that isn’t sufficient: the JVM has to have the ability to start executing the compiled version of the loop while the loop is still running. When the code for the loop has finished compiling, the JVM replaces the code (on-stack), and the next iteration of the loop will execute the much-faster compiled version of the code.

OSR编译的触发条件如下:

Server模式:

OSR trigger = (CompileThreshold * ((OnStackReplacePercentage - InterpreterProfilePercentage) / 100))
默认阈值:(10000 * ((140 - 33) / 100)) = 10700

Client模式:

OSR trigger = CompileThreshold * OnStackReplacePercentage / 100
默认阈值:1500 * 933 / 100 = 13995

OSR编译的执行过程如下图所示,当方法上的回边计数器到达阈值时,即触发后台的OSR编译,并将方法上累积的调用计数器设置为CompileThreshold的值,同时将回边计数器设置为CompileThreshold/2的值,一方面是为了避免OSR编译频繁触发;

另一方面是以便当方法被再次调用时即触发正常的编译,当累积的回边计数器的值再次达到该值时,先检查OSR编译是否完成。

如果OSR编译完成,则在执行循环体的代码时进入编译后的代码;如果OSR编译未完成,则继续把当前回边计数器的累积值再减掉一些,从这些描述可看出,默认情况下对于回边的情况,server模式下只要回边次数达到10700次,就会触发OSR编译。
OSR编译流程

CICompilerCount

CICompilerCount表示JIT执行代码编译的线程数,该参数的使用方式如下:

1
-XX:CICompilerCount=2

默认情况下,该参数的值会根据服务器的CPU数量进行调整,默认值可以参考下表:
CICompilerCount默认值配置

CodeCache

JVM里有一块比较特殊的内存叫做CodeCache,Java代码在执行时一旦被编译器编译为机器码,下一次执行的时候就会直接执行编译后的代码,也就是说,编译后的代码被缓存了起来。缓存编译后的机器码的内存区域就是CodeCache

这块内存主要存储JVM动态生成的代码,动态生成的代码有挺多,最主要的是JIT编译后的代码,Java之所以执行快,是因为随着程序的运行,大部分热点代码会被编译成优化过的机器码来执行,除了JIT编译的代码之外,动态生成的代码,**本地方法代码(JNI)**也会存在CodeCache中。

所以如果这块内存不够就会影响程序的执行效率。

CodeCache参数配置

关于CodeCache的相关参数主要有5个:InitialCodeCacheSize、ReservedCodeCacheSize、Xmaxjitcodesize、CodeCacheMinimumFreeSpace与UseCodeCacheFlushing,我们来逐个分析其作用。

InitialCodeCacheSize

InitialCodeCacheSize表示CodeCache区域的初始化大小,该参数的使用方式如下:

1
-XX:InitialCodeCacheSize=10M

CodeCache默认大小

默认情况下,在Liunx环境,该值的大小为2.4375M。

InitialCodeCacheSize是CodeCache初始化的时候的大小,但是随着CodeCache的增长不会降下来,但是CodeCache里的block是可以复用的。

ReservedCodeCacheSize

ReservedCodeCacheSize表示CodeCache的最大大小值,该参数的使用方式如下:

1
-XX:ReservedCodeCacheSize=240M

默认情况下,在Liunx环境,该值的大小为48M,如果开启了分层编译的话,默认是240M。

从JDK8开始,分层编译默认开启。

我们来验证一下是否是这样的:

开启分层编译的情况下:
ReservedCodeCacheSize默认大小
关闭分层编译的情况下:
ReservedCodeCacheSize默认大小
需要注意的是,ReservedCodeCacheSize必须大于InitialCodeCacheSize,且ReservedCodeCacheSize的大小不可以超过2048M,即2G:
ReservedCodeCacheSize最大值

Xmaxjitcodesize

Xmaxjitcodesize参数与ReservedCodeCacheSize的作用是等价的,均为设置CodeCache Size的最大大小,,该参数的使用方式如下:

1
-Xmaxjitcodesize20M

CodeCacheMinimumFreeSpace

CodeCacheMinimumFreeSpace表示CodeCache的可用大小不足这个值的时候,就会停止进行JIT编译,并进行code cache full的处理逻辑,该参数的使用方式如下:

1
-XX:CodeCacheMinimumFreeSpace=1M

默认情况下,在Liunx环境,该值的大小为500K。

code cache full一旦被处理,将会打印”CodeCache is full”的日志,但是这条日志只会打印一次:

1
2
3
Java HotSpot(TM) 64-Bit Server VM warning: CodeCache is full. Compiler has been disabled.
Java HotSpot(TM) 64-Bit Server VM warning: Try increasing the code cache size using -XX:ReservedCodeCacheSize=
Code Cache  [0xffffffff77400000, 0xffffffff7a390000, 0xffffffff7a400000) total_blobs=11659 nmethods=10690 adapters=882 free_code_cache=909Kb largest_free_block=502656

UseCodeCacheFlushing

一旦CodeCache达到了阈值,JVM将会切换到interpreted-only(解释执行)模式,字节码将不再会被编译成机器码。因此,应用程序将继续运行,但运行速度会降低一个数量级。

针对这种情况,可以通过参数-XX:+UseCodeCacheFlushing开启CodeCache自动清理机制,此时JVM会开始进行CodeCache区域的清理工作,卸载过期的旧code,以释放空间。

但是在JDK7版本中,CodeCache UseCodeCacheFlushing的机制也存在着一定的问题:
1、即使在清理完成后,CodeCache占用率下降到几乎一半之后,编译器也可能无法重新启动。
2、CodeCache的清理可能会导致编译器线程占用大量CPU,从而导致整体性能下降。

而在JDK8中,上面的问题已经被修复。

JIT注意事项

JIT是JVM的一个利器,可以让Java代码非常高效的执行,针对大多数应用服务场景,流量并非较小的场景下,JIT可以不必过度的关注,而面对大流量并发的场景下,JIT的性能则需要注意。当一个方法瞬时流量激增,会瞬间达到JIT编译的阈值,JVM会执行JIT编译,将热点代码编译成机器码,并进行缓存,但是当热点代码过多,JIT编译的压力会激增,带来的后果就是会将系统负载瞬时拉高,CPU使用也会飙升,导致整体服务整体的性能降低。

因此针对大流量并发的场景下,我个人的建议是,上线应用后,应预估好流量,小规模逐渐切流,避免瞬时的大流量大面积触发JIT编译,待JIT编译预热完毕后,逐渐切入全量流量,以此获得最佳的性能。

CodeCache注意事项

CodeCache这块内存区域,相对于我们耳熟能详的新老生代、本地元空间,往往是不太容易被注意到的地方,但是这个区域,却对我们的应用服务,起着非常重要的作用。

CodeCache是用来存储已编译方法生成的本地代码。CodeCache确实很少引起性能问题,但是一旦发生其影响可能是毁灭性的。如果CodeCache区域被占满,编译器被停用,字节码将不再会被编译成机器码,应用程序将继续运行,但运行速度会降低一个数量级,严重影响应用服务的运行

因此,CodeCache的大小配置是需要值得关注的,根据应用的实际运行情况,调整好CodeCache的区域大小,避免出现CodeCache堆满的情况发生。

结语

本篇我们了解了JVM中的另外一个比较重要的区域CodeCache,也了解了JVM中的动态编译机制JIT,针对大多数小流量的应用服务,对于这两个JVM的相关配置可以不用特别关注,但是针对大流量并非的互联网应用场景,则是需要关注的,配置合理合适的值,以保证服务的稳定运行。

本文参考:
Why do I get message “CodeCache is full. Compiler has been disabled”?
https://blogs.oracle.com/poonam/why-do-i-get-message-codecache-is-full-compiler-has-been-disabled

Oracle CodeCache Tuning
https://docs.oracle.com/javase/8/embedded/develop-apps-platforms/codecache.htm

Oracle Compilation Optimization

https://docs.oracle.com/javacomponents/jrockit-hotspot/migration-guide/comp-opt.htm#JRHMG117

JVM系列六(JVM-codecache内存区域介绍)
http://thinkhejie.github.io/2016/05/05/JVM%E7%B3%BB%E5%88%97_06/