一、项目背景
一个高并发系统中的抢购接口,高峰时 5W 的并发请求,且每次请求会产生 20KB 对象(包括订单、用户、优惠券等对象数据)。
我们可以通过一个并发创建一个 1MB 对象的接口来模拟万级并发请求产生大量对象的场景,具体代码如下:
二、准备工作
1.linux机器
我本机起一台 Linux 虚拟机,分配的内存为 2G,处理器数量为 2 个,如下
2.apache AB压测工具
安装和使用方法,此处不做过多介绍,参考传送门
3.JVM参数
查看JVM进程号
jps
监控JVM中minorGC和fullGC的时间、次数等信息
jstat -gc 进程号 5000 20 | awk '{print $13,$14,$15,$16,$17}'
查看JVM中堆内存使用情况
jmap -heap 进程号
三、压测步骤
首先启动项目
通过jmap -heap 进程号查看堆默认分配情况为:
最大堆内存为 480m、初始化大小为 32m。
Eden区:103m
From区、To区:3~4m
OLd区:18m
1.10 个并发用户/10万请求量(总)
使用ab命令压测
ab -c 10 -n 100000 http://127.0.0.1:8080/jvm/heap
执行结束后查看结果
查看gc情况
jstat -gc 9656 5000 20 | awk '{print $13,$14,$15,$16,$17 }'
通过两图结果可知
- 用户的吞吐量大于在 1426/每秒左右
- JVM 服务器平均请求处理时间 0.7ms 左右
- JVM 服务器发生了 2700 多次 YGC,耗时 15 秒 ;还有 45 次 FGC,2.3 秒左右。加在一起 GC 耗时 17 秒
2.100个并发用户/10万请求量(总)
步骤同上,下面直接说现象
- 用户的吞吐量大于在 1262/每秒左右
- JVM 服务器平均请求处理时间 0.8ms 左右
- JVM 服务器发生了 2700 多次 YGC,耗时 30 秒 ;还有 56 次 FGC,3 秒左右。加在一起 GC 耗时 33 秒
3.1000个并发用户/10万请求量(总)
步骤同上,下面直接说现象
- 用户的吞吐量大于在 1145/每秒左右
- JVM 服务器平均请求处理时间 0.8ms 左右
- JVM 服务器发生了 2700 多次 YGC,耗时 38 秒 ;还有 47 次 FGC,3 秒左右。加在一起 GC 耗时 42 秒
四、结果分析
- 堆内存
默认不配置堆内存大小和比例的情况下,JVM将采用默认的配置,而如果堆越小,minorGC次数就越多
- 吞吐量
GC耗时越久,吞吐量越低; fullGC越频繁,吞吐量越低
五、方案调整
通过上述结果,我们发现存在大量minorGC,虽然fullGC次数相比minorGC次数较少,但是也很影响性能。于是,我们调整堆大小,将堆内存设置大一点
方案一:调大堆空间,但是不设置SurvivorRatio
启动命令改为
java -jar -Xms1500m -Xmx1500m jvm-1.0-SNAPSHOT.jar
这里说句题外话。一般我们设置堆大小时,都是将最大堆和最小堆参数设置成一样,这样可以提前规划好固定内存,避免动态扩容,防止jvm运行过程中出现堆内存不够的情况。
通过jmap -heap 进程号查看堆默认分配情况为:
最大堆内存为 1.5G,初始化大小为 1.5G
Eden区:375m
From、To区:62m
OLd区:1000m
用与之前同样的压测方式,结果如下
1.10 个并发用户/10万请求量(总)
- 用户的吞吐量大于在 1205/每秒左右
- JVM 服务器平均请求处理时间 0.83ms 左右
- JVM 服务器发生了 800 次 YGC,耗时 33 秒 ,还有 1 次 FGC,1 秒左右,加在一起 GC 耗时 34 秒
2.100个并发用户/10万请求量(总)
- 用户的吞吐量大于在 989/每秒左右
- JVM 服务器平均请求处理时间 1.01ms 左右
- JVM 服务器发生了 800 次 YGC,耗时 46 秒 ,还有 8 次 FGC,6 秒左右,加在一起 GC 耗时 52 秒
3.1000个并发用户/10万请求量(总)
- 用户的吞吐量大于在 749/每秒左右
- JVM 服务器平均请求处理时间 1.3ms 左右
- JVM 服务器发生了 800 次 YGC,耗时 66 秒 ,还有 8 次 FGC,9 秒左右,加在一起 GC 耗时 75 秒
方案二:调大堆空间,同时设置SurvivorRatio
启动命令改为
java -jar -Xms1500m -Xmx1500m -Xmn1000m -XX:SurvivorRatio=8 jvm-1.0-SNAPSHOT.jar
通过jmap -heap 进程号查看堆默认分配情况为:
最大堆内存为 1.5G,初始化大小为 1.5G
Eden 区 800m
From、To 100M
OLd =500M
用与之前同样的压测方式,结果如下
1.10 个并发用户/10万请求量(总)
- 用户的吞吐量大于在 1780/每秒左右
- JVM 服务器平均请求处理时间 0.56ms 左右
- JVM 服务器发生了 400 次 YGC,耗时 5.8 秒 ,还有 2 次 FGC,0.1 秒左右,加在一起 GC 耗时 6 秒
2.100个并发用户/10万请求量(总)
- 用户的吞吐量大于在 1927/每秒左右
- JVM 服务器平均请求处理时间 0.51ms 左右
- JVM 服务器发生了 400 多次 YGC,耗时 11 秒 ,没有 FGC,加在一起 GC 耗时 11 秒
3.1000个并发用户/10万请求量(总)
- 用户的吞吐量大于在 1657/每秒左右
- JVM 服务器平均请求处理时间 0.6ms 左右
- JVM 服务器发生了 400 多次 YGC,耗时 14 秒 ,还 1 次 FGC,3 秒左右,加在一起 GC 耗时 17 秒
六、结果总结和分析
在方案一中,虽然调大了堆空间,但是吞吐量和GC耗时的结果相比没有调大时,反而还差了点;
方案二不仅调大了堆空间,还设置了eden和survivor的比例,在所有结果无疑是最好的。
为什么会出现这个情况呢?不管是方案一还是方案二,可以确定的是业务场景里每个对象存活对象时间都是固定的。
而在方案一中,堆空间是增大了,但eden区域并没有很大,因此eden区域还是很快被填满,并开始minorGC。minorGC时大量对象可能还是存活的,因此GC时间为:扫描整个堆空间+复制存活对象。复制存活对象会采用复制算法,是一个很耗时的操作;同时,由于堆空间相比没有任何调整前还大,因此扫描整个堆空间的时间也更长,导致方案一的GC耗时和吞吐量也更低。
在方案二中,eden区域很大,因此出现minorGC的频率相比方案一更低。同时开始minorGC时,大量对象可能都已过了存活时间,因此GC时间为:只扫描整个堆空间,而没有复制存活对象的时间。虽然扫描整个堆空间的时间也更长,但是相比复制的成本,可以忽略。
所以方案二由于方案一。