- Java锁的逻辑(结合对象头和ObjectMonitor)
- 还在用饼状图?来瞧瞧这些炫酷的百分比可视化新图形(附代码实现)⛵
- 自动注册实体类到EntityFrameworkCore上下文,并适配ABP及ABPVNext
- 基于Sklearn机器学习代码实战
作者:京东科技 徐传乐 。
在高并发下,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;ls |grep "^[0-9]"|awk ' $0 >100') ;do awk '/Swap:/{a=a+$2}END{print '"$i"',a/1024"M"}' /proc/$i/smaps 2>/dev/null ; done | sort -k2nr | head -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?
a、 临时设置方案,重启后不生效 。
sysctl vm.swappiness=0 。
cat /proc/sys/vm/swappiness 。
b、 永久设置方案,重启后仍然生效 。
vi /etc/sysctl.conf 。
vm.swappiness=0 。
前提:首先要保证内存剩余要大于等于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/24/hbase-linux/ 。
解答是不是只要开启了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
fi
done | 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系统,在内存分配上要慎重.
以上内容希望可以起到抛转引玉的作用,如有理解不到位的地方烦请指出.
最后此篇关于一次JVMGC长暂停的排查过程的文章就讲到这里了,如果你想了解更多关于一次JVMGC长暂停的排查过程的内容请搜索CFSDN的文章或继续浏览相关文章,希望大家以后支持我的博客! 。
我有一个如下所示的数据框: import pandas as pd d = {'decil': ['1. decil','1. decil','2. decil','2. decil','3. dec
我有一些数据想要添加到我的应用中...大约 650 个类别(包括名称 + ID 号),每个类别平均有 85 个项目(每个都有一个名称/ID 号)。 iPhone会支持这么大的plist吗?我想首先在
我目前正在使用 Python 从头开始实现决策树算法。我在实现树的分支时遇到了麻烦。在当前的实现中,我没有使用深度参数。 发生的情况是,要么分支结束得太快(如果我使用标志来防止无限递归),要么如果
我在 Stack 上发现了这个问题 - Measuring the distance between two coordinates in PHP 这个答案在很多方面似乎对我来说都是完美的,但我遇到了
我目前正在清理一个具有 2 个索引和 2.5 亿个事件行以及大约同样多(或更多)的死行的表。我从我的客户端计算机(笔记本电脑)向我的服务器发出命令 VACCUM FULL ANALYZE。在过去的 3
这一切都有点模糊,因为该计划是相当深入的,但坚持我,因为我会尽量解释它。我编写了一个程序,它接受一个.csv文件,并将其转换为MySQL数据库的INSERT INTO语句。例如: ID Numbe
我有一个地址示例:0x003533,它是一个字符串,但要使用它,我需要它是一个 LONG,但我不知道该怎么做:有人有解决方案吗? s 字符串:“0x003533”到长 0x003533 ?? 最佳答案
请保持友善 - 这是我的第一个问题。 =P 基本上作为一个暑期项目,我一直在研究 wikipedia page 上的数据结构列表。并尝试实现它们。上学期我参加了 C++ 类(class),发现它非常有
简单的问题。想知道长 IN 子句是否是一种代码味道?我真的不知道如何证明它。除了我认为的那样,我不知道为什么它会闻起来。 select name, code, capital, pop
我正在尝试基于 C# 中的种子生成一个数字。唯一的问题是种子太大而不能成为 int32。有什么方法可以像种子一样使用 long 吗? 是的,种子必须很长。 最佳答案 这是我移植的 Java.Util.
我一直想知道这个问题有一段时间了。在 CouchDB 中,我们有一些相当的日志 ID……例如: “000ab56cb24aef9b817ac98d55695c6a” 现在,如果我们正在搜索此项目并浏览
列的虚拟列 c和一个给定的值 x等于 1如果 c==x和 0 其他。通常,通过为列创建虚拟对象 c , 一排除一个值 x选择,因为最后一个虚拟列不添加任何信息 w.r.t.已经存在的虚拟列。 这是我如
使用 tarantool,为什么我要记录这些奇怪的消息: 2016-03-24 16:19:58.987 [5803] main/493623/http/XXX.XXX.XXX.XXX:57295 t
我显然是 GitHub 的新手,想确保在开始之前我做的事情是正确的。 我想创建一个新的存储库,它使用来自 2 个现有项目的复刻/克隆。现有项目不是我的。 假设我想使用的 repo 被称为来自开发人员“
我的应用程序名称长度为 17 个字符。当安装在设备上时,它看起来像应用程序...名称。有没有办法在多行上显示应用程序名称?请帮忙。 最佳答案 不,你不能。我认为 iPad 支持 15 个字符来完整显示
我必须编写一个程序来读取文件中的所有单词,并确定每个单词使用了多少次。我的任务是使用多线程来加快运行时间,但是单线程程序的运行速度比多线程程序快。我曾尝试研究此问题的解决方案,但很多解释只会让我更加困
假设我在给定的范围内有一个位置pos,这样: 0 = newRange*newRange : "Case not supported yet"; // Never happens in my code
我试图在 Java 中将 unix 时间四舍五入到该月的第一天,但没有成功。示例: 1314057600 (Tue, 23 Aug 2011 00:00:00 GMT) 至 1312156800
我们的项目有在 CVS 中从现有分支创建新分支的历史。几年后,这导致了每次发布时更改的文件上的这种情况: 新版本:1.145.4.11.2.20.2.6.2.20.2.1.2.11.2.3.2.4.4
我有以下数据框: DAYS7 <- c('Monday','Tuesday','Wednesday','Thursday','Friday', 'Saturday', 'Sunday') DAYS
我是一名优秀的程序员,十分优秀!