当前位置: 首页 > 后端技术 > Java

一个JVMGClong-pause排查过程

时间:2023-04-01 18:09:04 Java

作者:京东科技许传乐背景在高并发下,Java程序的GC问题是一类非常典型的问题,其影响往往会被进一步放大。无论是“GC频率过快”还是“GC时间过长”,由于GC时的StopTheWorld问题,很容易造成服务超时,造成性能问题。一开始问题是某线上应用的垃圾回收出现了FullGC的异常现象。在某些应用实例中,FullGC时间很长,大约持续15-30秒,平均每2周左右触发一次;JVM参数配置“-Xms2048M–Xmx2048M–Xmn1024M–XX:MaxPermSize=512M”故障排除流程?分析GC日志GC日志记录了每次GC的执行时间和执行结果。通过分析GC日志,您可以调整堆设置和GC设置,或者改进应用程序的对象分配模式。这里FullGC的原因是Ergonomics,因为开启了UseAdaptiveSizePolicy,FullGC是jvm的自适应调整导致的,这个日志主要反映GC前后的变化,目前看不出为什么。要启用GC日志,需要添加以下JVM启动参数:-XX:+PrintGCDetails-XX:+PrintGCDateStamps-Xloggc:/export/log/risk_pillar/gc.log常见的YoungGC和FullGC日志都有含义如下:?进一步查看服务器性能指标获取GC耗时后,通过监控平台获取各项监控项,开始查看该时间点的异常指标。最终分析发现,在5.06分钟左右(GC时间点),CPU使用率明显上升,同时SWAP释放了资源,内存资源增长达到拐点(详见下图红框,橙色框内的变化是修改配置引起的,后面会介绍,暂时可以忽略)JVMUsedswap?是不是因为GC导致CPU突然飙升,swap区的内存被释放到内存了?为了验证JVM是否使用swap,我们查看proc下的进程内存资源使用情况foriin$(cd/proc;lsgrep"^[0-9]"awk'$0>100');doawk'/Swap:/{a=a+$2}END{print'"$i"',a/1024"M"}'/proc/$i/smaps2>/dev/null;donesort-k2nrhead-10#head-10表示取出前10个内存占用高的进程#取出的第一列是进程的id第二列占进程的swapsize确实有用305MB的swap下面简单介绍一下什么是swap?swap是指交换分区或文件,主要是在内存使用有压力时触发内存回收。此时可能会将部分内存数据交换到swap空间,这样系统就不会因为内存不足导致oom或者更致命的情况。.当进程向OS申请内存,发现内存不足时,OS会将内存中暂时不用的数据换出,放到交换分区中。这个过程称为换出。当一个进程再次需要这些数据并且OS发现还有空闲的物理内存时,它会将交换分区中的数据交换回物理内存。这个过程称为换入。为了验证GC耗时与swap操作有一定关系,我随机抽查了十几台机器,重点查看GC耗时日志,确认GC耗时时间点和swap操作时间点确实是通过时间点一致的。进一步查看虚拟机各个实例的swappiness参数,一个普遍的现象是所有长FullGC的实例都配置了参数vm.swappiness=30(值越大越倾向于使用swap);而GC时间比较正常的实例配置参数vm.swappiness=0(尽量减少swap的使用)。swappiness可以设置为0到100之间的值。它是Linux的一个内核参数,控制系统进行swap时内存使用的相对权重。?swappiness=0:表示物理内存使用到最大,然后swap空间?swappiness=100:表示swap分区被积极使用,及时将内存上的数据交换到swap空间。内存使用和swap使用情况如下至此,手指好像指向了swap。?问题分析当内存使用达到水位(vm.swappiness)时,Linux会将一些暂时不用的内存数据放入磁盘swap中,以释放更多的可用内存空间;当swap区的数据需要使用时,JVM进行GC时,需要遍历对应堆分区的已用内存;如果在GC的时候,有一部分堆被交换到交换空间,这部分内存在遍历的时候需要交换回内存,因为需要访问磁盘,所以相对于物理内存,其速度肯定慢得要命,GC暂停时间会非常非常可怕;从而导致Linux在swap分区的恢复上滞后(Memorytodiskswappinginandswappingout操作消耗大量的CPU和系统IO),而在高并发/QPS业务中,这种滞后的结果是致命的(STW).?至此问题解决了,答案似乎很明确了,我们只需要尝试关闭或释放swap,看看是否能解决问题?如何释放swap?设置vm.swappiness=0(重启应用释放swap后生效),意思是swap内存尽量不要使用a。临时设置方案,重启后不生效。cat/proc/sys/vm/swappinessb,永久设置scheme,重启后仍然生效vi/etc/sysctl.confaddvm.swappiness=0关闭swap分区swapoff–a前提:先保证剩余内存大于等于swapusage,否则会报Cannotallocatememory!一旦swap分区被释放,swap分区中存储的所有文件都会被转移到物理内存中,这可能会导致系统IO或其他问题。A。检查当前交换分区挂载在哪里?b.关闭分区。关闭swap区后的内存变化如下图橙色框所示。此时swap分区中的文件全部转移到物理内存中。发生了FullGC,耗时190ms,问题解决。?疑惑1.只要启用了swap区的JVM,GC会很长时间吗?2、既然JVM这么不喜欢swap,为什么JVM不明确禁止它的使用呢?3、swap的工作机制是什么?这台物理内存为8G的服务器使用的是交换内存(swap),说明物理内存不够用。但是通过free命令查看内存使用情况,实际物理内存似乎并没有占用那么多,但是Swap却占了将近1G。?free:除了buff/cache还剩多少内存shared:共享内存buff/cache:buffer和cache内存的数量(如果使用率过高,通常是程序频繁访问文件)available:真正剩余的availablememory,你可以想一想,关闭swapdiskcache是??什么意思?其实大可不必如此激进。要知道这个世界从来就不是0就是1,每个人或多或少都会选择在中间,只是有的偏向0,有的偏向1。显然,在swap的问题上,JVM可以选择尽量少用,减少swap的影响。要想减少swap的影响,就要搞清楚linux内存回收是如何进行的,以免遗漏任何可能的疑惑。我们先来看看swap是怎么触发的?Linux会在两种情况下触发内存回收。一种是在内存分配过程中发现空闲内存不足时,立即触发内存回收;另一种是启动一个守护进程(kswapd进程)来定期检查系统内存。,在可用内存低于特定阈值后主动触发内存回收。通过下图可以很容易理解。更多信息请参考:http://hbasefly.com/2017/05/2...回答swap区的JVM是否会在GC时被消耗很久之后,笔者查看了另外一个应用,相关指标信息如下图所示。实名服务的QPS非常高。也可以看出应用了swap,GC平均耗时576ms。这是为什么呢?把时间范围集中在发生GC的某段时间,从监控指标图中可以看到swapUsed没有变化,也就是说没有swap活动,这不会影响花在垃圾收集上的总时间。使用以下命令列出每个进程占用的交换空间。很明显,实名服务占用的交换空间更少(仅54.2MB)。另一个值得注意的现象是实名服务的fullGC间隔时间短(每隔几个小时),我的服务FullGC平均间隔2周swap分区的数据不需要换回GC期间的物理内存。它完全基于内存计算,因此速度要快得多。2.内存数据被替换到swap区的筛选策略应该类似于LRU算法(最近最少使用原则)为了证实上面的猜测,我们只需要跟踪swapchangelog,监听数据变化到得到答案。这里我们使用shell脚本来实现#!/bin/bashecho-e`date+%y%m%d%H%M%S`echo-e"PID\t\tSwap\t\tProc_Name"#取出/proc目录下所有以数字命名的目录(进程名是数字就是进程,其他如sys、net等存放其他信息)forpidin`ls-l/proc|grep^d|awk'{打印$9}'|grep-v[^0-9]`doif[$pid-eq1];thencontinue;figrep-q"Swap"/proc/$pid/smaps2>/dev/nullif[$?-eq0];thenswap=$(gawk'/Swap/{sum+=$2;}END{printsum}'/proc/$pid/smaps)#统计swap分区占用的大小单位为KBproc_name=$(psaux|grep-w"$pid"|awk'!/grep/{for(i=11;i<=NF;i++){printf("%s",$i);}}')#取出进程名if[$swap-gt0];then#判断swap是否被占用只有被占用才会输出echo-e"${pid}\t${swap}\t${proc_name:0:100}"fifidone|排序-k2nr|头-10|gawk-F'\t''{#sortthetop10pid[NR]=$1;尺寸[NR]=$2;姓名[NR]=$3;}END{for(id=1;id<=length(pid);id++){if(size[id]<1024)printf("%-10s\t%15sKB\t%s\n",pid[id],大小[id],名称[id]);elseif(size[id]<1048576)printf("%-10s\t%15.2fMB\t%s\n",pid[id],size[id]/1024,name[id]);否则printf("%-10s\t%15.2fGB\t%s\n",pid[id],size[id]/1048576,name[id]);}}'由于上图中2022.3.219:57:00到2022.3.219:58:00发生了一次FullGC,我们只关注这一分钟内swap区的变化即可。我每10秒收集一次信息。可以看到GC时间前后swap没有变化。通过以上分析,回到本文的核心问题。现在看来,我的处理方式太激进了。事实上,关闭掉期是没有必要的。通过适当减小堆大小,也可以解决问题。这也说明,部署Java服务的Linux系统在内存分配上并不是无脑。大而全,需要综合考虑JVM在不同场景下对Java永久代、Java堆(新生代和老年代)、线程栈、JavaNIO使用内存的要求。总结综上所述,我们得出的结论是swap和GC同时发生,导致GC时间非常长,JVM卡顿严重,极端情况下会导致服务崩溃。主要原因是:JVM在进行GC时,需要遍历对应堆分区的已用内存。如果在GC的时候,堆的一部分被swap到swap,遍历这部分的时候需要把它换回内存;更极端的情况,由于同时内存空间不足,内存中的另一部分堆需要改为SWAP。因此在遍历堆分区的过程中,会依次将整个堆分区写入SWAP,导致GC时间超长。交换区的大小应该在线限制。如果swap使用率高,应检查并解决。在适当的时候,您可以减小堆大小或添加物理内存。因此,部署Java服务的Linux系统在内存分配上一定要慎重。希望以上内容能起到转身引玉的作用。如有不明白之处欢迎指出。