bapiera

导航

JVM之codecache

       之前在做压力测试的时候,经常遇到一种场景,就是有些java应用程序刚刚启动,就模拟场景向系统发压,但压力发起时性能非常差,压测结果不达标。然而第二次再测试时,性能就可以达到一个稳定的高水位。当时只知道热点代码被“缓存”起来不需要重复编译,不知道其中的原理,在查询一些资料之后。。。

CodeCache

CodeCache是热点代码的暂存区,经过即时编译器编译的代码会放在这里,它存在于堆外内存。除了JIT编译的代码(占用了大部分)之外,Java所使用的本地方法代码(JNI)也会存在codeCache中

编译器

       Client编译器(C1):面向对启动性能有要求的客户端GUI程序,采用的优化手段比较简单,因此编译的时间较短

       Server编译器(C2):面向对性能峰值有要求的服务端程序,采用的优化手段复杂,因此编译时间长,但是在运行过程中性能更好

       C1的优势在于不用等待,C2在实际运行当中效率更高

       分层编译:从Java7开始,JVM默认采用分层编译的方式:热点方法首先被C1编译器编译,而后热点方法中的热点再进一步被C2编译(理解为二次编译,根据前面的运行计算出更优的编译优化)。为了不干扰程序的正常运行,JIT编译时放在额外的线程中执行的,HotSpot根据实际CPU的资源,以1:2的比例分配给C1和C2线程数。在计算机资源充足的情况,字节码的解释运行和编译运行时可以同时进行,编译执行完后的机器码会在下次调用该方法时启动,已替换原本的解释执行(意思就是已经翻译出效率更高的机器码,自然替换原来的相对低效率执行的方法)

CodeCache区域的GC

JVM内部会先尝试解释执行Java字节码,当方法调用或循环回边达到一定次数时(C1:1500;C2:10000),会触发即时编译,将Java字节码编译成本地机器码以提高执行效率。

这个编译的本地机器码是缓存在CodeCache中的,如果有大量的代码触发了即时编译,而且没有及时GC的话,CodeCache就会被填满。一旦CodeCache被填满,已经被编译的代码还会以本地代码方式执行,但后面没有编译的代码只能以解释执行的方式运行。系统处理能力明显下降。

       JVM针对CodeCache提供了GC方式: -XX:+UseCodeCacheFlushing

       在JDK1.7.0_4之后这个参数默认开启,当CodeCache即将填满时会尝试回收。JDK7在这方面的回收做的不好,GC收益较低,在JDK8有了很大的改善。

       在Java8中提供了一个JVM启动参数:-XX:+PrintCodeCache,他可以在JVM停止时打印CodeCache的使用情况,可以在每次停止应用时观察一下这个值,慢慢调整为一个最合适的大小。通过在启动参数上增加:-XX:+UseCodeCacheFlushing 来启用。

       打开这个选项后:

  1. 在JIT被关闭之前,也就是CodeCache装满之前,会在JIT关闭前做一次清理,删除一些CodeCache的代码。如果清理后还是没有空间,那么JIT依然会关闭。
  2. 当codeCache将要耗尽时,最早被编译的一半方法将会被放到一个old列表中等待回收;在一定时间间隔内,如果old列表中方法没有被调用,这个方法就会被从codeCache充清除;Code Cache满了时紧急进行清扫工作,它会丢弃一半老的编译代码;
  3. Code Cache空间降了一半,方法编译工作仍然可能不会重启;
  4. flushing可能导致高的cpu使用,从而影响性能下降;

       JDK 8对codeCache的回收有了很明显的改善。不仅codeCache的增长比较平缓,而且当使用量达到75%时,回收力度明显加大,codeCache使用量在这个值上下浮动,并缓慢增长。最重要的是,JIT编译还在正常执行,系统运行速度也没有收到影响。



posted on 2020-11-18 22:41  柯南道尔的江户川  阅读(680)  评论(0编辑  收藏  举报