目录
1、问题描述
在日常巡检时发现,线上部分服务器的性能会出现波动,tp等指标出现飙升。经过一系列的排查,最终发现是由于C2编译线程长时间运间消耗了CPU.
异常的堆栈信息如下:
"C2 CompilerThread11" #17 daemon prio=9 os_prio=0 tid=0x00007fdd8c6fc800 nid=0x133 waiting on condition [0x0000000000000000]
java.lang.Thread.State: RUNNABLE
jdk版本如下:
java version "1.8.0_20"
Java(TM) SE Runtime Environment (build 1.8.0_20-b26)
Java HotSpot(TM) 64-Bit Server VM (build 25.20-b23, mixed mode)
2、问题解决过程
(1) 是否因gc引起
先看一下一台预发机的性能。
确实,这台机器是由于gc导致的,在性能升高的点因Metadata GC Threshold导致的full gc。对应的gc日志如下:
2021-03-09T16:54:16.228+0800: 429252.720: [Full GC (Metadata GC Threshold) 1291M->110M(4096M), 1.2160712 secs]
[Eden: 816.0M(2824.0M)->0.0B(2864.0M) Survivors: 40.0M->0.0B Heap: 1291.7M(4096.0M)->110.6M(4096.0M)], [Metaspace: 96237K->95604K(1136640K)]
Heap after GC invocations=106 (full 4):
garbage-first heap total 4194304K, used 113234K [0x00000006c0000000, 0x00000007c0000000, 0x00000007c0000000)
region size 4096K, 0 young (0K), 0 survivors (0K)
Metaspace used 95604K, capacity 96816K, committed 98304K, reserved 1136640K
class space used 10374K, capacity 10684K, committed 11008K, reserved 1048576K
}
从JDK8开始,永久代被废弃了,取而代之的是一个称为Metaspace的存储空间,它使用的是本地内存而不是堆内存,也就是说在默认情况下它的大小只与本地内存大小有关。虽然Metaspace区的最大值(在没有主动设置-XX:MaxMetaspaceSize)只与本地内存有关,但是-XX:MetaspaceSize
是有初始默认值的,可以通过java -XX:+PrintFlagsFinal 或者 -XX:+PrintFlagsInitials查看初始参数值(uintx MetaspaceSize = 21807104)。
为了避免Metadata GC导致的full gc, 合理设置metaspace的大小,并将MetaspaceSize
和MaxMetaspaceSize
设置一样大即可,在我的应用中设置了256M足矣。
(2) 线上机器的性能
生产环境的jvm参数中已经避免了Metadata GC Threshold导致的full gc。在巡检过程中,发现并没有频繁的ygc以及因full gc导致的性能问题,继续深入分析该问题。首先确认是否codeCache是否被填满导致的性能,通过非堆内存的监控(非堆内存最大值为1.3G,已经使用107M)发现服务器的CodeCache并没有使用满。
JVM在编译代码的时候,会在CodeCache中保存一些汇编后指令。由于其大小是固定的,一旦被填满了,就会导致部分热点代码没法被编译,JVM就无法编译其他代码,应用的性能将会急剧下降(基于解释性编译执行代码)。
Java HotSpot(TM) 64-Bit Server VM warning: CodeCacheis full. Compiler has been disabled.
Java HotSpot(TM) 64-Bit Server VM warning: Tryincreasing the code cache size using -XX:ReservedCodeCacheSize=
在我所使用jvm的code cache默认大小为240M。
-XX:ReservedCodeCacheSize=251658240(jinfo -flag ReservedCodeCacheSize pid)
继续排查原因(由于每隔几天出现一次性能问题,显然就不能归究于网络抖动),通过top及jstack查看最耗CPU的线程堆栈,发现是0X144,具体的看问题描述部分。
Tasks: 493 total, 0 running, 493 sleeping, 0 stopped, 0 zombie
Cpu(s): 21.4%us, 6.0%sy, 0.0%ni, 71.6%id, 0.0%wa, 0.0%hi, 1.0%si, 0.0%st
Mem: 263727056k total, 234156976k used, 29570080k free, 3490188k buffers
Swap: 16777212k total, 55228k used, 16721984k free, 97451304k cached
PID USER PR NI VIRT RES SHR S %CPU %MEM TIME+ COMMAND
307 admin 20 0 36.9g 2.4g 12m S 14.3 0.9 16:55.77 java
454 admin 20 0 36.9g 2.4g 12m S 1.0 0.9 9:29.54 java
455 admin 20 0 36.9g 2.4g 12m S 1.0 0.9 9:48.97 java
下面我们就开始聊聊JIT编译。
JIT编译类型有C1编译器,C2编译器,分层编译器。这里我简单介绍一下分层编译的5个层次,其他的就略了……
分层编译将JVM执行的状态分为5个层次:
- 解释执行
- 执行不带profiling的C1代码
- 执行仅带方法调用次数以及循环回边执行次数profiling的C1代码
- 执行带所有profiling的C1代码
- 执行C2代码
profiling其实就是收集能够反映程序执行状态的数据,比如方法的调用次数,以及循环回边的执行次数等。
通常情况下,C2代码的执行效率要比C1代码的高出30%以上。C1层执行的代码,按执行效率排序从高至低则是1层>2层>3层。这5个层次中,1层和4层都是终止状态,当一个方法到达终止状态后,只要编译后的代码并没有失效,那么JVM就不会再次发出该方法的编译请求的。服务实际运行时,JVM会根据服务运行情况,从解释执行开始,选择不同的编译路径,直到到达终止状态。下图中就列举了几种常见的编译路径:
- 图中第①条路径,代表编译的一般情况,热点方法从解释执行到被3层的C1编译,最后被4层的C2编译。
- 如果方法比较小(比如Java服务中常见的getter/setter方法),3层的profiling没有收集到有价值的数据,JVM就会断定该方法对于C1代码和C2代码的执行效率相同,就会执行图中第②条路径。在这种情况下,JVM会在3层编译之后,放弃进入C2编译,直接选择用1层的C1编译运行。
- 在C1忙碌的情况下,执行图中第③条路径,在解释执行过程中对程序进行profiling ,根据信息直接由第4层的C2编译。
- 前文提到C1中的执行效率是1层>2层>3层,第3层一般要比第2层慢35%以上,所以在C2忙碌的情况下,执行图中第④条路径。这时方法会被2层的C1编译,然后再被3层的C1编译,以减少方法在3层的执行时间。
- 如果编译器做了一些比较激进的优化,比如分支预测,在实际运行时发现预测出错,这时就会进行反优化,重新进入解释执行,图中第⑤条执行路径代表的就是反优化。
这段出自 :Java即时编译器原理解析及实践
在jdk8中,默认是开启分层编译,TieredCompilation编译确实可以在启动时平滑CPU负载,但分层编译是综合考虑C1和C2编译器的优点衍生出来的一种折中编译器,在一些特殊情况下可以采用了比较激进甚至不可靠的优化方法,如出现分支预测失败,重而进行反优化重新进入解释执行。
因此,线上的服务器部分关闭分层编译,启用C2编译:-XX:-TieredCompilation,对比观察观察(由于我们的服务器访问量级QPM:100,如果用分层编译,感觉刚达到优化层级,又不调用,反倒浪费优化时间)【当然对于量级较大的应用,开启分层编译还是有很大优势的】。
上图是对比图,唯一区别就在于:绿色是:-XX:+TieredCompilation, 红色:-XX:-TieredCompilation。