C2 CompilerThread9 长时间占用CPU解决方案
  01BFOGI7NzGp 2023年12月12日 20 0

一、问题描述
近期在进行日常巡检时发现,线上部分应用服务器的CPU突然比以往高出很多,经过登录机器排查确认是C2 CompilerThread9线程始终长时间运行消耗了CPU。

排查步骤在上篇博文有记录总结,地址:排查CPU异常步骤_u012538947的专栏

异常线程的堆栈如下:

"C2 CompilerThread9" #48 daemon prio=9 os_prio=0 tid=0x00007f45f0b80000 nid=0x188 runnable [0x0000000000000000]
java.lang.Thread.State: RUNNABLE
补充描述:我的应用类型为后台接口服务,系统秒级调用峰值在10W+,JRE版本如下:

java version "1.8.0_60"
Java(TM) SE Runtime Environment (build 1.8.0_60-b27)
Java HotSpot(TM) 64-Bit Server VM (build 25.60-b23, mixed mode)
二、问题解决过程
思路1:修改codeCache的默认大小(结论:没有效果)
什么是CodeCache?(主要参考博文:Code Cache满导致应用性能降低_weixin_34203426的博客

Java代码在执行次数达到一个阈值会触发JIT编译,一旦代码块被编译成本地机器码,下次执行的时候会直接运行编译后的本地机器码。所以这本地机器码必须被缓存起来,而缓存这个本地机器码的内存区域就是Code Cache,它并不属于Java堆的一部分,除了JIT编译的代码之外,Java所使用的本地方法代码(JNI)也会存在codeCache中。

通过查阅了网上的N多资料,了解到当CodeCache空间使用完之后,JVM的JIT功能会被停止,将不会编译任何额外的代码;被编译过的代码仍然以编译方式执行,但是尚未编译的代码智能以解释的方式执行了。在这种情况下,如果应用中很多代码以解释方式执行,系统性能势必会大大降低,第一感觉貌似就是这个问题,于是开始修改JVM启动参数,验证方案是否可行。

步骤一:通过jinfo命令查看当前默认的CodeCache大小,得到的结论与网上JDK版本对应的默认大小一致,为240M。具体命令如下,pid为Java进程ID。

jinfo -flag ReservedCodeCacheSize pid
步骤二:查看当前CodeCache使用情况,这一步异常坎坷,由于线上堡垒机权限问题,网上的很多办法都没有成功。具体的方式大概有5种,分别如下:

jvm启动参数加上-XX:+PrintCodeCache,可以在jvm关闭时输出code cache的使用情况
使用jcmd的Compiler.codecache,其输出跟-XX:+PrintCodeCache相同;
使用jcmd的VM.native_memory也可以查看code cache的使用情况(Code部分)
使用JMX来获取NON_HEAP类型中的name为CodeHeap开头的MemoryPoolMXBean可以得到code cache的使用情况
如果是springboot应用,它使用micrometer,通过/actuator/metrics接口提供相关指标查询功能,其中code cache在jvm.memory.used这个metric中
具体参考地址:聊聊jvm的Code Cache - 简书

步骤三:设置JVM启动参数,死马当活马医。设置CodeCache大小为300M,具体参数如下:

-XX:ReservedCodeCacheSize=300M
很遗憾,重启后运行一段时间发现,系统CPU负载依然会升高。后来通过公司的统一监控平台证明了我的CodeCache并没有使用满,由于CodeCache是属于堆外内存,然而我的系统堆外缓存使用一共还不到200M,所以也从一方面证明了思路一的方向是错误的。非堆内存使用大小截图如下:

 

C2 CompilerThread9 长时间占用CPU解决方案_JVM

 

如果计划尝试思路一的方法来解决问题,建议大家先想办法证明应用的CodeCache确实使用满了,正常来说JDK8 版本的默认值240M已经足够用了,如果其他版本的JRE环境可能确实会有问题。如果无法证明满了,也可以像我一样死马当活马医,试试就知道结果了,但是不要抱太大希望。

思路2:关闭JIT分层编译(结论:有效果)
在继续寻找解决方案的过程中,在网上看到一个案例,和我的JRE环境完全相同,问题现象完全相同,案例中通过关闭JIT分层编译解决了问题,于是我也尝试了这一方案,实践证明问题得到了解决。案例地址:https://community.oracle.com/message/15472264#15472264

步骤一:了解JIT编译原理

因为之前对JIT的编译原理并不了解,不敢随意修改线上服务器的编译类型,担心会有一些其他的副作用,所以在网上开始了查阅资料学习的过程。

什么是JIT编译?

编译器在编译过程中通常会考虑很多因素。比如:汇编指令的顺序。假设我们要将两个寄存器的值进行相加,执行这个操作一般只需要一个CPU周期;但是在相加之前需要将数据从内存读到寄存器中,这个操作是需要多个CPU周期的。编译器一般可以做到,先启动数据加载操作,然后执行其它指令,等数据加载完成后,再执行相加操作。由于解释器在解释执行的过程中,每次只能看到一行代码,所以很难生成上述这样的高效指令序列。而编译器可以事先看到所有代码,因此,一般来说,解释性代码比编译性代码要慢。

java 作为静态语言十分特殊,他需要编译,但并不是在执行之前就编译为本地机器码。Java的实现在解释性和编译性之间进行了折中,Java代码是编译性的,它会被编译成一个平台独立的字节码程序。JVM负责加载、解释、执行这些字节码程序,在这个过程中,还可能会将这些字节码实时编译成目标机器码,以便提升性能。

所以,在谈到 java的编译机制的时候,其实应该按时期,分为两个部分。一个是 javac指令 将java源码变为 java字节码的静态编译过程。 另一个是 java字节码编译为本地机器码的过程,并且因为这个过程是在程序运行时期完成的所以称之为即时编译(JIT:Just In Time)。

JIT编译类型:C1编译器、C2编译器、分层编译器。

通常我们说即时编译器有两种类型,Client Compiler(C1编译器)和Server Compiler(C2编译器)。这两种编译器最大的区别就是,编译代码的时间点不一样。C1编译器会更早的对代码进行编译,因此在程序刚启动的时候,C1编译器比C2编译器执行的更快,所以C1编译器适用于一些GUI应用,可以缩短应用启动时间。C2编译器会收集更多的信息,然后才对代码进行编译优化,所以从长远角度考虑,C2编译器最终可以产生比C1编译器更优秀的代码,适用于长时间运行的后台接口服务。

可能大家都有一个困扰,JVM为什么要将编译器分为client和server,为什么不在程序启动时,使用client编译器,在程序运行一段时间后,自动切换为server编译器? 其实,这种技术是存在的,一般称之为 Tiered Compiler(分层编译器)。Java7 和Java 8可以使用选项-XX:+TieredCompilation来打开(-server选项也要打开)。在Java 8中,-XX:+TieredCompilation默认是打开的。

分层编译将 JVM 的执行状态分为了 5 个层次:

第 0 层:程序解释执行,默认开启性能监控功能(Profiling),如果不开启,可触发第二层编译;

第 1 层:可称为 C1 编译,将字节码编译为本地代码,进行简单、可靠的优化,不开启 Profiling;

第 2 层:也称为 C1 编译,开启 Profiling,仅执行带方法调用次数和循环回边执行次数 profiling 的 C1 编译;

第 3 层:也称为 C1 编译,执行所有带 Profiling 的 C1 编译;

第 4 层:可称为 C2 编译,也是将字节码编译为本地代码,但是会启用一些编译耗时较长的优化,甚至会根据性能监控信息进行一些不可靠的激进优化。
在一些特殊情况下,激进优化后的代码并不能有更高的性能。需要进行优化回退,将重新对代码进行解释执行。
通过以上资料学习,我们得出结论:

C2编译器相对于C1编译器更适用于我们系统
分层编译器是综合考虑C1和C2编译器的优点衍生出的一种进化版本编译器,但是由于我们是纯后台应用,这种衍生优化是否有效未可知。
分层编译器在一些特殊情况下可能比较激进、不可靠。
JIT学习参考博文:

Java性能优化指南系列(三):理解JIT编译器_qq_28674045的博客-

浅谈对JIT编译器的理解。 - stubbornnnnnnn 

Java 面试-即时编译( JIT ) - jianjianqq

步骤二:关闭分层编译,启用C2编译器

JVM启动脚本中添加如下参数

-XX:-TieredCompilation -server
关闭分层编译后,我们对线上服务器的性能情况进行了监控比较,性能几乎没有什么区别,甚至更优于原来的分层编译(猜测是因为系统CPU负载下降了导致)。性能截图如下(红色曲线为关闭分层编译的机器性能走势图):

C2 CompilerThread9 长时间占用CPU解决方案_编译器_02

 

 

启用分层编译时的CPU负载截图:

 

C2 CompilerThread9 长时间占用CPU解决方案_JVM_03

 

关闭分层编译后的CPU负载截图:

C2 CompilerThread9 长时间占用CPU解决方案_编译器_04

 


【版权声明】本文内容来自摩杜云社区用户原创、第三方投稿、转载,内容版权归原作者所有。本网站的目的在于传递更多信息,不拥有版权,亦不承担相应法律责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@moduyun.com

  1. 分享:
最后一次编辑于 2023年12月12日 0

暂无评论

推荐阅读
  bVJlYTdzny4o   6天前   16   0   0 Java
01BFOGI7NzGp