作者:京东科技 徐传乐

背景

在高并发下,Java程序的GC问题属于很典型的一类问题,带来的影响往往会被进一步放大。不论是「GC频率过快」还是「GC耗时太长」,因为GC期间都存在Stop The World问题,因而很容易导致服务超时,引发性能问题。

事件最后是线上某利用垃圾收集呈现Full GC异样的景象,利用中个别实例Full GC工夫特地长,持续时间约为15~30秒,均匀每2周左右触发一次;

JVM参数配置“-Xms2048M –Xmx2048M –Xmn1024M –XX:MaxPermSize=512M”

排查过程

Ø 剖析GC 日志

GC 日志它记录了每一次的 GC 的执行工夫和执行后果,通过剖析 GC 日志能够调优堆设置和 GC 设置,或者改良应用程序的对象分配模式。

这里Full GC的reason是Ergonomics,是因为开启了UseAdaptiveSizePolicy,jvm本人进行自适应调整引发的Full GC。

这份日志次要体现GC前后的变动,目前为止看不出个所以然来。

开启GC日志,须要增加如下 JVM 启动参数:

-XX:+PrintGCDetails -XX:+PrintGCDateStamps -Xloggc:/export/log/risk_pillar/gc.log

常见的 Young GC、Full GC 日志含意如下:

Ø 进一步查看服务器性能指标

获取到了GC耗时的工夫后,通过监控平台获取到各个监控项,开始排查这个时点有异样的指标,最终剖析发现,在5.06分左右(GC的时点),CPU占用显著晋升,而SWAP呈现了开释资源、memory资源增长呈现拐点的状况(详见下图红色框,橙色框中的变动是因批改配置导致,前面会介绍,暂且可疏忽)

JVM用到了swap?是因为GC导致的CPU忽然飙升,并且开释了swap替换区这部分内存到memory?

为了验证JVM是否用到swap,咱们通过查看proc下的过程内存资源占用状况

for i in $( cd /proc;lsgrep "^[0-9]"awk ' $0 >100') ;do awk '/Swap:/{a=a+$2}END{print '"$i"',a/1024"M"}' /proc/$i/smaps 2>/dev/null ; donesort -k2nrhead -10 # head -10 示意 取出 前10个内存占用高的过程 # 取出的第一列为过程的id 第二列过程占用swap大小

看到的确有用到305MB的swap

这里简略介绍下什么是swap?

swap指的是一个替换分区或文件,次要是在内存应用存在压力时,触发内存回收,这时可能会将局部内存的数据交换到swap空间,以便让零碎不会因为内存不够用而导致oom或者更致命的状况呈现。

当某过程向OS申请内存发现有余时,OS会把内存中临时不必的数据交换进来,放在swap分区中,这个过程称为swap out。

当某过程又须要这些数据且OS发现还有闲暇物理内存时,又会把swap分区中的数据交换回物理内存中,这个过程称为swap in。

为了验证GC耗时与swap操作有必然关系,我抽查了十几台机器,重点关注耗时长的GC日志,通过工夫点确认到GC耗时的工夫点与swap操作的工夫点的确是统一的。

进一步查看虚拟机各实例 swappiness 参数,一个普遍现象是,但凡产生较长Full GC的实例都配置了参数 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的时候,有堆的一部分内容被替换到swap空间中,遍历到这部分的时候就须要将其替换回内存,因为须要拜访磁盘,所以相比物理内存,它的速度必定慢的令人发指,GC进展的工夫肯定会十分十分恐怖;

进而导致Linux对swap分区的回收滞后(内存到磁盘换入换出操作非常占用CPU与零碎IO),在高并发/QPS服务中,这种滞后带来的后果是致命的(STW)。

Ø 问题解决

至此,答案仿佛很清晰,咱们只需尝试把swap敞开或开释掉,看看是否解决问题?

如何开释swap?

  1. 设置vm.swappiness=0(重启利用开释swap后失效),示意尽可能不应用替换内存

a、 长期设置计划,重启后不失效

设置vm.swappiness为0

sysctl vm.swappiness=0

查看swappiness值

cat /proc/sys/vm/swappiness

b、 永恒设置计划,重启后依然失效

vi /etc/sysctl.conf

增加

vm.swappiness=0

  1. 敞开替换分区swapoff –a

前提:首先要保障内存残余要大于等于swap使用量,否则会报Cannot allocate memory!swap分区一旦开释,所有寄存在swap分区的文件都会转存到物理内存上,可能会引发零碎IO或者其余问题。

a、 查看以后swap分区挂载在哪?

b、 关停分区

敞开swap替换区后的内存变动见下图橙色框,此时swap分区的文件都转存到了物理内存上

敞开Swap替换区后,于2.23再次发生Full GC,耗时190ms,问题失去解决。

Ø 纳闷

1、 是不是只有开启了swap替换区的JVM,在GC的时候都会耗时较长呢?

2、 既然JVM对swap如此不待见,为何JVM不明令禁止应用呢?

3、 swap工作机制是怎么的?这台物理内存为8g的server,应用了替换区内存(swap),阐明物理内存不够应用了,然而通过free命令查看内存应用状况,理论物理内存仿佛并没有占用那么多,反而Swap已占近1G?

free:除了buff/cache残余了多少内存

shared:共享内存

buff/cache:缓冲、缓存区内存数(应用过高通常是程序频繁存取文件)

available:实在残余的可用内存数


大家能够想想,敞开替换磁盘缓存意味着什么?

其实大可不必如此激进,要晓得这个世界永远不是非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流动,进而没有影响到垃级回收的总耗时。

通过如下命令列举出各过程swap空间占用状况,很分明的看到实名这个服务swap空间占用的较少(仅54.2MB)

另一个显著的景象是实名服务Full GC距离较短(几个小时一次),而我的服务均匀距离2周一次Full GC

基于以上揣测

1、 实名服务因为 GC 距离较短,内存中的货色基本没有机会置换到swap中就被回收了,GC的时候不须要将swap分区中的数据交换回物理内存中,齐全基于内存计算,所以要快很多

2、 将哪些内存数据置换进swap替换区的筛选策略应该是相似于LRU算法(最近起码应用准则)

为了证实上述猜想,咱们只需跟踪swap变更日志,监控数据变动即可失去答案,这里采纳一段shell 脚本实现

#!/bin/bash echo -e `date +%y%m%d%H%M%S` echo -e "PID\t\tSwap\t\tProc_Name" #拿出/proc目录下所有以数字为名的目录(过程名是数字才是过程,其余如sys,net等寄存的是其余信息) for pid in `ls -l /proc | grep ^d | awk '{ print $9 }'| grep -v [^0-9]` do     if [ $pid -eq 1 ];then continue;fi     grep -q "Swap" /proc/$pid/smaps 2>/dev/null     if [ $? -eq 0 ];then         swap=$(gawk '/Swap/{ sum+=$2;} END{ print sum }' /proc/$pid/smaps) #统计占用的swap分区的 大小 单位是KB         proc_name=$(ps aux | grep -w "$pid" | awk '!/grep/{ for(i=11;i<=NF;i++){ printf("%s ",$i); }}') #取出过程的名字         if [ $swap -gt 0 ];then #判断是否占用swap 只有占用才会输入             echo -e "${pid}\t${swap}\t${proc_name:0:100}"     fi    fidone | sort -k2nr | head -10 | gawk -F'\t' '{ #排序取前 10     pid[NR]=$1;     size[NR]=$2;     name[NR]=$3; } END{     for(id=1;id<=length(pid);id++)     {     if(size[id]<1024)         printf("%-10s\t%15sKB\t%s\n",pid[id],size[id],name[id]);     else if(size[id]<1048576)         printf("%-10s\t%15.2fMB\t%s\n",pid[id],size[id]/1024,name[id]);    else     printf("%-10s\t%15.2fGB\t%s\n",pid[id],size[id]/1048576,name[id]);     } }'

因为下面图中 2022.3.2 19:57:00 至 2022.3.2 19:58:00 产生了一次Full GC,咱们重点关注下这一分钟内swap替换区的变动即可,我这里每10s做一次信息采集,能够看到在GC时点前后,swap的确没有变动

通过上述剖析,回归本文外围问题上,当初看来我的解决形式过于激进了,其实也能够不必敞开swap,通过适当升高堆大小,也是可能解决问题的。

这也侧面的阐明,部署Java服务的Linux零碎,在内存调配上并不是无脑大而全,须要综合思考不同场景下JVM对Java永恒代 、Java堆(新生代和老年代)、线程栈、Java NIO所应用内存的需要。

总结

综上,咱们得出结论,swap和GC同一时候产生会导致GC工夫十分长,JVM重大卡顿,极其的状况下会导致服务解体。

次要起因是:JVM进行GC时,须要对对应堆分区的已用内存进行遍历,如果GC的时候,有堆的一部分内容被替换到swap中,遍历到这部分的时候就须要将其替换回内存;更极其状况同一时刻因为内存空间有余,就须要把内存中堆的另外一部分换到SWAP中去,于是在遍历堆分区的过程中,会把整个堆分区轮流往SWAP写一遍,导致GC工夫超长。线上应该限度swap区的大小,如果swap占用比例较高应该进行排查和解决,适当的时候能够通过升高堆大小,或者增加物理内存。

因而,部署Java服务的Linux零碎,在内存调配上要谨慎。

以上内容心愿能够起到抛转引玉的作用,如有了解不到位的中央烦请指出。