以一个创建 1MB 对象的接口来模拟万级并发请求产生大量对象的场景。
java
@RequestMapping(value = "/test1")
public String test1(HttpServletRequest request) {
List<Byte[]> temp = new ArrayList<Byte[]>();
Byte[] b = new Byte[1024*1024];
temp.add(b);
return "success";
}
AB 压测
shell
ab -n <请求数> -c <并发数> 'http://127.0.0.1:8080/test1'
分别对应用服务进行压力测试,以下是请求接口的吞吐量和响应时间在不同并发用户数下的变化情况:
并发请求数 | 响应时间(Time per request) | 吞吐量(Requests per second) |
---|---|---|
10/10000 | 5.971 | 1674.74 |
30/10000 | 20.105 | 1551.20 |
50/10000 | 31.154 | 1602.10 |
70/10000 | 57.319 | 1185.61 |
90/10000 | 70.585 | 1278.02 |
100/10000 | 96.512 | 1040.35 |
120/10000 | 109.021 | 1095.57 |
总体来看,当并发数量到了一定值时,吞吐量就上不去了,响应时间也迅速增加。
分析 GC 日志
此时我们可以通过查看具体的 GC 日志,设置 VM 配置参数,将运行期间的 GC 日志 dump 下来,具体配置参数如下:
cpp
-XX:+PrintGCTimeStamps -XX:+PrintGCDetails -Xloggc:heapTest.log
收集到 GC 日志后,使用 GCViewer
工具打开,查看到具体的 GC 日志如下:
主页面显示 FullGC 发生了 8 次,右图显示年轻代和老年代的内存使用率几乎达到了 100%。
而 FullGC 会导致 stop-the-world
的发生,从而严重影响到应用服务的性能。
此时,我们需要调整堆内存的大小来减少 FullGC 的发生。
参考指标
GC 频率
高频的 FullGC 会给系统带来非常大的性能消耗,虽然 MinorGC 相对 FullGC 来说好了许多,但过多的 MinorGC 仍会给系统带来压力。
内存
此内存指的是堆内存大小,堆内存又分为年轻代内存 和老年代内存。
首先我们要分析堆内存大小是否合适,其实是分析年轻代和老年代的比例是否合适。
如果内存不足或分配不均匀,会增加 FullGC,严重的将导致 CPU 持续爆满,影响系统性能。
吞吐量
频繁的 FullGC 将会引起线程的上下文切换,增加系统的性能开销,从而影响每次处理的线程请求,最终导致系统的吞吐量下降。
延时
JVM 的 GC 持续时间也会影响到每次请求的响应时间。
具体调优方法
调整堆内存空间减少 FullGC:通过日志分析,堆内存基本被用完了,而且存在大量 FullGC,这意味着堆内存严重不足,需要调大堆内存空间。
cpp
-Xms3g -Xmx3g
调大堆内存之后,再来测试下在 100 并发下的性能情况,发现吞吐量提高了 40% 左右,响应时间也降低了将近 50%。
再查看 GC 日志,发现 GC 频率降低了,老年代的使用率只有 2% 了。
调整年轻代减少 MinorGC:通过调整堆内存大小,我们已经提升了整体的吞吐量,降低了响应时间。
还可以将年轻代设置得大一些,从而减少一些 MinorGC。
cpp
-Xms3g -Xmx3g -Xmn2g
查看 GC 日志,发现 MinorGC次数 明显降低了,GC 花费的总时间也减少了。
设置 Eden、Survivor 区比例: 在 JVM 中,如果开启 AdaptiveSizePolicy
,则每次 GC 后都会重新计算 Eden
、From Survivor
和 To Survivor
区的大小,计算依据是 GC 过程中统计的 GC 时间、吞吐量、内存占用量,这个时候 SurvivorRatio
默认设置的比例会失效。
在 JDK1.8 中,默认是开启 AdaptiveSizePolicy
的,可以通过 -XX:-UseAdaptiveSizePolicy
关闭该项配置,或显示运行 -XX:SurvivorRatio=8
将 Eden、Survivor 的比例设置为 8:2。
大部分新对象都是在 Eden 区创建的,我们可以固定 Eden 区的占用比例,来调优 JVM 的内存分配性能。
通过上图可以看到关闭AdaptiveSizePolicy
后,吞吐量提升了,响应时间降低了,GC 停顿次数和停顿时间都降低了。