JVM性能调优之内存调优与GC优化

JVM 性能调优之内存优化与 GC 优化

JVM 调优是一个系统而又复杂的过程,但我们知道,在大多数情况下,我们基本不用去调整 JVM 内存分配,因为一些初始化的参数已经可以保证应用服务正常稳定地工作了。
在应用服务的特定场景下,JVM 内存分配不合理带来的性能表现并不会像内存溢出问题这么突出。一般你没有深入到各项性能指标中去,是很难发现其中隐藏的性能损耗。

压测工具 AB

Ab(ApacheBench) 测试工具是 Apache 提供的一款测试工具,具有简单易上手的特点,在测试 Web 服务时非常实用。
ab 一般都是在 Linux 上用。
安装非常简单,只需要在 Linux 系统中输入 yum -y install httpd-tools 命令,就可以了。
安装成功后,输入 ab 命令,可以看到以下信息:
注意压测的同时观察堆得gc信息是变化的,查看完需要计算平均耗时。
JVM性能调优之内存调优与GC优化_第1张图片
ab 工具用来测试 post get 接口请求非常便捷,可以通过参数指定请求数、并发数、请求参数等

测试 get 请求接口
ab -c 10 -n 100 localhost:8080/test/login?userName=test&password=test

测试 post 请求接口
ab -n 100 -c 10 -p ‘post.txt’ -T ‘application/x-www-form-urlencoded’ ‘localhost:8080/test/register’
post.txt 为存放 post 参数的文档,存储格式如usernanme=test&password=test&sex=1

参数的含义:
-n:总请求次数(最小默认为 1);
-c:并发次数(最小默认为 1 且不能大于总请求次数,例如:10 个请求,10 个并发,实际就是 1 人请求 1 次);
-p:post 参数文档路径(-p 和 -T 参数要配合使用);
-T:header 头内容类型(此处切记是大写英文字母 T);

输出中,性能指标参考
JVM性能调优之内存调优与GC优化_第2张图片
Requests per second:吞吐率,指某个并发用户数下单位时间内处理的请求数;
Time per request:上面的是用户平均请求等待时间,指处理完成所有请求数所花费的时间 /(总请求数 / 并发用户数);
Time per request:下面的是服务器平均请求处理时间,指处理完成所有请求数所花费的时间 / 总请求数;
Percentage of the requests served within a certain time:每秒请求时间分布情况,指在整个请求中,每个请求的时间长度的分布情况,例如有 50% 的请求响应在 16ms 内,66% 的请求响应在 23ms 内。

系统默认的堆栈分配使用命令 jmap -heap pid

JVM性能调优之内存调优与GC优化_第3张图片
压测机器,linux centos7 2核CPU,2G内存,需要安装jdk,并上传需要的测试jar包。
使用ab压测10个并发,10w的访问量的测试结果
JVM性能调优之内存调优与GC优化_第4张图片
JVM性能调优之内存调优与GC优化_第5张图片

查看GC耗时使用命令 jstat -gc pid 停顿时间 打印次数
| awk ‘{print $13,$14,$15,$16,$17 }’ 打印指定的列
在这里插入图片描述

JVM性能调优之内存调优与GC优化_第6张图片

测试结果显示:
用户的吞吐量大约在 1487/每秒左右
JVM 服务器平均请求处理时间 0.6ms 左右
JVM 服务器发生了 2700 多次 YGC,耗时 16秒 ,还有 49次 FGC,1.6秒左右,加在一起 GC 耗时 18 秒左右

ab -c 100 -n 100000 http://127.0.0.1:8080/jvm/heap

JVM性能调优之内存调优与GC优化_第7张图片

此时查看堆内存信息,发现jvm内存在动态扩容。
JVM性能调优之内存调优与GC优化_第8张图片
JVM性能调优之内存调优与GC优化_第9张图片

测试结果显示:
 用户的吞吐量大于在 1279/每秒左右
 JVM 服务器平均请求处理时间 0.8ms 左右
JVM 服务器发生了 2700 多次 YGC,耗时 36秒 ,还有 64 次 FGC,2.5 秒左右,加在一起 GC 耗时 39 秒

ab -c 1000 -n 100000 http://127.0.0.1:8080/jvm/heap
JVM性能调优之内存调优与GC优化_第10张图片

测试结果显示:
 用户的吞吐量大于在 1108/每秒左右
 JVM 服务器平均请求处理时间 0.9ms 左右
JVM 服务器发生了2700 多次 YGC,耗时 48秒 ,还有 46 次 FGC,4 秒左右,加在一起 GC 耗时51 秒

结果分析
GC 频率
高频的 FullGC 会给系统带来非常大的性能消耗,虽然 MinorGC 相对 FullGC 来说好了许多,但过多的 MinorGC 仍会给系统带来压力。
内存
这里的内存指的是堆内存大小,堆内存又分为年轻代内存和老年代内存。堆内存不足,会增加 MinorGC ,影响系统性能。
吞吐量
频繁的 GC 将会引起线程的上下文切换,增加系统的性能开销,从而影响每次处理的线程请求,最终导致系统的吞吐量下降。
延时
JVM 的 GC 持续时间也会影响到每次请求的响应时间。

调优方案

调整方案一
调整堆内存空间减少 GC:通过分析,堆内存基本被用完了,而且存在大量 MinorGC 和 FullGC,这意味着我们的堆内存严重不足,这个时候我们需要调大堆内存空间。
堆空间加大到 1.5G
java -jar -Xms1500m -Xmx1500m jvm-1.0-SNAPSHOT.jar

JVM性能调优之内存调优与GC优化_第11张图片
使用 AB 进行压力测试:
ab -c 10 -n 100000 http://127.0.0.1:8080/jvm/heap
JVM性能调优之内存调优与GC优化_第12张图片
JVM性能调优之内存调优与GC优化_第13张图片

测试结果显示:
 用户的吞吐量大于在 1040/每秒左右
 JVM 服务器平均请求处理时间 0.9ms 左右
JVM 服务器发生了800 多次 YGC,耗时 48秒 ,还有 2次 FGC,2 秒左右,加在一起 GC 耗时52秒

ab -c 100 -n 100000 http://127.0.0.1:8080/jvm/heap
JVM性能调优之内存调优与GC优化_第14张图片
JVM性能调优之内存调优与GC优化_第15张图片
总结:YGC900多次,耗时40s左右,FGC9次,耗时2.4s左右,总共GC时间43s左右

ab -c 1000 -n 100000 http://127.0.0.1:8080/jvm/heap
JVM性能调优之内存调优与GC优化_第16张图片
JVM性能调优之内存调优与GC优化_第17张图片
测试结果显示:
 用户的吞吐量大于在 907/每秒左右
 JVM 服务器平均请求处理时间 1.1ms 左右
JVM 服务器发生了1100 多次 YGC,耗时 58秒 ,还有 30次 FGC,7 秒左右,加在一起 GC 耗时65秒

调整方案二

java -jar -Xms1500m -Xmx1500m -Xmn1000m -XX:SurvivorRatio=8 jvm-1.0-SNAPSHOT.jar

JVM性能调优之内存调优与GC优化_第18张图片
压测前先让机器热身,并发量小一点压测,慢慢往上加。
使用 AB 进行压力测试:
ab -c 10 -n 100000 http://127.0.0.1:8080/jvm/heap
JVM性能调优之内存调优与GC优化_第19张图片

测试结果显示:
 用户的吞吐量大于在 1799/每秒左右
 JVM 服务器平均请求处理时间 0.56ms 左右
JVM 服务器发生了 400 次 YGC,耗时6 秒 ,还有 2 次 FGC,0.1 秒左右,加在一起 GC 耗时 6.1秒

ab -c 100 -n 100000 http://127.0.0.1:8080/jvm/heap

JVM性能调优之内存调优与GC优化_第20张图片
JVM性能调优之内存调优与GC优化_第21张图片
测试结果显示:
 用户的吞吐量大于在 1635/每秒左右
 JVM 服务器平均请求处理时间 0.61ms 左右
JVM 服务器发生了 400 次 YGC,耗时17 秒 ,还有 13 次 FGC,2.4 秒左右,加在一起 GC 耗时 19秒

ab -c 1000 -n 100000 http://127.0.0.1:8080/jvm/heap
JVM性能调优之内存调优与GC优化_第22张图片
JVM性能调优之内存调优与GC优化_第23张图片
测试结果显示:
 用户的吞吐量大于在 2033/每秒左右
 JVM 服务器平均请求处理时间 0.49ms 左右
JVM 服务器发生了 400 次 YGC,耗时11 秒 ,还有 1次 FGC,0.6 秒左右,加在一起 GC 耗时 12秒

内存优化总结

一般情况下,高并发业务场景中,需要一个比较大的堆空间,而默认参数情况下,堆空间不会很大。所以我们有必要进行调整。但是不要单纯的调整堆的总大小,要调整新生代和老年代的比例,以及 Eden 区还有 From 区,还有 To 区的比例。

所以在我们上述的测试中,调整方案二,得到结果是最好的。在三种测试情况下都能够有非常好的性能指标,同时 GC 耗时相对控制也较好。对于调整方案一,就是单纯的加大堆空间,里面的比例不适合高并发场景,反而导致堆空间变大,没有明显减少 GC 的次数,但是每次 GC 需要检索对象的堆空间更大,所以 GC 耗时更长。

方案二:调整为一个很大的新生代和一个较小的老年代.原因是,这样可以尽可能回收掉大部分短期对象,减少中期的对象,而老年代尽存放长期存活对象。由于新生代空间较小,Eden 区很快被填满,就会导致频繁 Minor GC,因此我们可以通过增大新生代空间来降低 Minor GC 的频率。单次 Minor GC 时间是由两部分组成:T1(扫描新生代)和 T2(复制存活对象)。
默认 情况 :一个对象在 Eden 区的存活时间为 500ms,Minor GC 的时间间隔是 300ms,因为这个对象存活时间>间隔时间,那么正常情况下,MinorGC 的时间为 :T1+T2。

方案一 :整堆空间加大,但是新生代没有增大多少,对象在 Eden 区的存活时间为 500ms,Minor GC 的时间可能会扩大到 400ms,因为这个对象存活时间>间隔时间,那么正常情况下,Minor GC 的时间为 :T1*1.5(Eden 区加大了)+T2

方案二:当我们增大新生代空间,Minor GC 的时间间隔可能会扩大到 600ms,此时一个存活 500ms 的对象就会在 Eden 区中被回收掉,此时就不存在复制存活对象了,所以再发生 Minor GC 的时间为:即 T12(空间大了)+T20可见,扩容后,Minor GC 时增加了 T1,但省去了 T2 的时间。在 JVM 中,复制对象的成本要远高于扫描成本。如果在堆内存中存在较多的长期存活的对象,此时增加年轻代空间,反而会增加 Minor GC 的时间。如果堆中的短期对象很多,那么扩容新生代,单次 Minor GC 时间不会显著增加。因此,单次 Minor GC 时间更多取决于 GC 后存活对象的量,而非 Eden区的大小。
这个就解释了之前的内存调整方案中,方案一为什么性能还差些,但是到了方案二话,性能就有明显的上升。

推荐策略

1. 新生代大小选择
1) 响应时间优先的应用:尽可能设大,直到接近系统的最低响应时间限制(根据实际情况选择).在此种情况下,新生代收集发生的频率也是最小的.同时,减少到达老年代的对象.。
2)吞吐量优先的应用:尽可能的设置大,可能到达 Gbit 的程度.因为对响应时间没有要求,垃圾收集可以并行进行,一般适合 8CPU 以上的应用.避免设置过小.当新生代设置过小时会导致:1.MinorGC 次数更加频繁 2.可能导致 MinorGC 对象直接进入老年代,如果此时老年代满了,会触发 FullGC.。

2. 老年代大小选择
响应时间优先的应用:老年代使用并发收集器,所以其大小需要小心设置,一般要考虑并发会话率和会话持续时间等一些参数.如果堆设置小了,可以会造成内存碎片,高回收频率以及应用暂停而使用传统的标记清除方式;如果堆大了,则需要较长的收集时间.最优化的方案,一般需要参考以下数据获得:并发垃圾收集信息、持久代并发收集次数、传统 GC 信息、花在新生代和老年代回收上的时间比例。
吞吐量优先的应用:一般吞吐量优先的应用都有一个很大的新生代和一个较小的老年代.原因是,这样可以尽可能回收掉大部分短期对象,减少中期的对象,而老年代尽存放长期存活对象。

GC优化

GC 性能衡量指标

吞吐量:
这里的衡量吞吐量是指应用程序所花费的时间和系统总运行时间的比值。我们可以按照这个公式来计算 GC 的吞吐量:系统总运行时间 = 应用程序耗时+GC 耗时。如果系统运行了 100 分钟,GC 耗时 1 分钟,则系统吞吐量为 99%。GC 的吞吐量一般不能低于 95%。

停顿时间:
指垃圾回收器正在运行时,应用程序的暂停时间。对于串行回收器而言,停顿时间可能会比较长;而使用并发回收器,由于垃圾收集器和应用程序交替运行,程序的停顿时间就会变短,但其效率很可能不如独占垃圾收集器,系统的吞吐量也很可能会降低。

垃圾回收频率:
通常垃圾回收的频率越低越好,增大堆内存空间可以有效降低垃圾回收发生的频率,但同时也意味着堆积的回收对象越多,最终也会增加回收时的停顿时间。所以我们需要适当地增大堆内存空间,保证正常的垃圾回收频率即可。

分析 GC 日志
通过 JVM 参数预先设置 GC 日志,几种 JVM 参数设置如下:
-XX:+PrintGC 输出 GC 日志
-XX:+PrintGCDetails 输出 GC 的详细日志
-XX:+PrintGCTimeStamps 输出 GC 的时间戳(以基准时间的形式)
-XX:+PrintGCDateStamps 输出 GC 的时间戳(以日期的形式,如 2013-05-04T21:53:59.234+0800)
-XX:+PrintHeapAtGC 在进行 GC 的前后打印出堆的信息
-Xloggc:…/logs/gc.log 日志文件的输出路径

比如:java -jar -XX:+PrintGCDateStamps -XX:+PrintGCDetails -Xloggc:./gc2logs -Xms1500m -Xmx1500m -Xmn1000m
-XX:SurvivorRatio=8 jvm-1.0-SNAPSHOT.jar
linux环境压测时,指定日志输出目录,再通过日志分析工具进行数据分析。 gcViewer

GC 调优策略

降低 Minor GC 频率

由于新生代空间较小,Eden 区很快被填满,就会导致频繁 Minor GC,因此我们可以通过增大新生代空间来降低 Minor GC 的频率。
单次 Minor GC 时间是由两部分组成:T1(扫描新生代)和 T2(复制存活对象)。
情况 1:假设一个对象在 Eden 区的存活时间为 500ms,Minor GC 的时间间隔是 300ms,因为这个对象存活时间>间隔时间,那么正常情况下,MinorGC 的时间为 :T1+T2。
情况 2:当我们增大新生代空间,Minor GC 的时间间隔可能会扩大到 600ms,此时一个存活 500ms 的对象就会在 Eden 区中被回收掉,此时就不存在复制存活对象了,所以再发生 Minor GC 的时间为:即 T12(空间大了)+T20
可见,扩容后,Minor GC 时增加了 T1,但省去了 T2 的时间。
在 JVM 中,复制对象的成本要远高于扫描成本。如果在堆内存中存在较多的长期存活的对象,此时增加年轻代空间,反而会增加 Minor GC 的时间。如果堆中的短期对象很多,那么扩容新生代,单次 Minor GC 时间不会显著增加。因此,单次 Minor GC 时间更多取决于 GC 后存活对象的数量,而非 Eden区的大小。
这个就解释了之前的内存调整方案中,方案一为什么性能还差些,但是到了方案二话,性能就有明显的上升。

降低 Full GC 的频率

由于堆内存空间不足或老年代对象太多,会触发 Full GC,频繁的 Full GC 会带来上下文切换,增加系统的性能开销。
减少创建大对象 :在平常的业务场景中,我们一次性从数据库中查询出一个大对象用于 web 端显示。比如,一次性查询出 60 个字段的业务操作,这种大对象如果超过年轻代最大对象阈值,会被直接创建在老年代;即使被创建在了年轻代,由于年轻代的内存空间有限,通过 Minor GC 之后也会进入到老年代。这种大对象很容易产生较多的 Full GC。
增大堆内存空间:在堆内存不足的情况下,增大堆内存空间,且设置初始化堆内存为最大堆内存,也可以降低 Full GC 的频率。

选择合适的 GC 回收器

如果要求每次操作的响应时间必须在 500ms 以内。这个时候我们一般会选择响应速度较快的 GC 回收器,堆内存比较小的情况下(<6G)选择 CMS(Concurrent Mark Sweep)回收器和堆内存比较大的情况下(>8G)G1 回收器.

总结

GC 调优是个很复杂、很细致的过程,要根据实际情况调整,不同的机器、不同的应用、不同的性能要求调优的手段都是不同的,这些都需要我们自己平时去积累,去观察,去实践。一般调优的思路都是“测试 - 分析 - 调优”三步走。
最后,给大家提个醒,任何调优都需要结合场景,明确已知问题和性能目标,不能为了调优而调优,以免引入新的 Bug,带来风险和弊端。

你可能感兴趣的:(jvm,jvm)