JVM 9:性能调优之内存优化

1.压测工具 ApacheBench

  • Linux执行yum -y install httpd-tools命令进行安装
    在这里插入图片描述
  • 安装成功后执行ab命令
    在这里插入图片描述
  • 测试 get 请求接口
ab -c 1000 -n 100000 http://127.0.0.1:8080/jvm/heap?userName=test&password=test
  • 测试 post 请求接口
ab -c 1000 -n 100000 -p 'post.txt' -T 'application/x-www-form-urlencoded' 'http://127.0.0.1:8080/jvm/heap'

post.txt 为存放 post 参数的文档,存储格式如 usernanme=test&password=test&sex=1

  • 参数的含义:
    -n:总请求次数(最小默认为 1);
    -c:并发次数(最小默认为 1 且不能大于总请求次数,例如:10 个请求,10 个并发,实际就是 1 人请求 1 次);
    -p:post 参数文档路径(-p 和 -T 参数要配合使用);
    -T:header 头内容类型(此处切记是大写英文字母 T);
  • 输出中,性能指标参考
    在这里插入图片描述
    Requests per second:吞吐量,指某个并发用户数下单位时间内处理的请求数;
    Time per request:上面的是用户平均请求等待时间,指处理完成所有请求数所花费的时间 /(总请求数 / 并发用户数);
    Time per request:下面的是服务器平均请求处理时间,指处理完成所有请求数所花费的时间 / 总请求数;
    Percentage of the requests served within a certain time:每秒请求时间分布情况,指在整个请求中,每个请求的时间长度的分布情况,例如有 50% 的请求响应在 101ms 内,66% 的请求响应在 134ms 内,说明有 16% 的请求在 101ms~134ms 之间。

2.内存分配案例

  • 一个高并发系统中的抢购接口,高峰时 5W 的并发请求,且每次请求会产生 20KB 对象(包括订单、用户、优惠券等对象数据)。 这里通过一个并发创建一个 1MB 对象的接口来模拟万级并发请求产生大量对象的场景,具体代码如下(最简单的springboot工程,只包含一个controller)
    在这里插入图片描述

2.1AB 压测

对应用服务进行压力测试,模拟不同并发用户数下的服务的响应情况:
1、100 个并发用户/10 万请求量(总),ab -c 100 -n 100000 http://127.0.0.1:8080/jvm/heap
2、1000 个并发用户/10 万请求量(总),ab -c 1000 -n 100000 http://127.0.0.1:8080/jvm/heap

2.2服务器信息

  • 虚拟机分配内存2G,处理器数量2个
    在这里插入图片描述

2.3GC 监控

jstat -gc 16148 5000 20 | awk '{print $13,$14,$15,$16,$17}'

16148为应用的进程号,{print $13,$14,$15,$16,$17}表示只显示gc信息的第13列到17列

在这里插入图片描述

2.4堆空间监控

  • jmap -heap 16148
    在这里插入图片描述
  • 在默认不配置 JVM 堆内存大小的情况下,JVM 根据默认值来配置当前内存大小。 我们可以通过以下命令来查看堆内存配置的默认值: java -XX:+PrintFlagsFinal -version | grep HeapSize
    在这里插入图片描述
    启动的 JVM 默认最大堆内存为 480MB,初始化大小为 32MB。

2.5压测

  • 将jar包上传至服务器,执行java -jar jvm-1.0-SNAPSHOT.jar启动应用程序

在这里插入图片描述

2.5.1 100 个并发用户/10 万请求量(总)
  • 执行ab -c 100 -n 100000 http://127.0.0.1:8080/jvm/heap,第一次的结果作为热身,忽略。
    运行时堆的动态分配结果,jdk1.8默认使用ParallerGC垃圾回收器,参数-XX:+UseAdaptiveSizePolicy默认开启,此时新生代的大小会动态调整,不是默认的8:1:1。
    在这里插入图片描述
    并发请求后的结果
    在这里插入图片描述
    计算gc次数和时间,第一次压测结果作为热身,不做参考,只作为初始值;jstat统计的结果均为总数,所以计算时需要用2次结果求差值。
    在这里插入图片描述

测试结果:
用户的吞吐量为 1606/每秒左右
JVM 服务器平均请求处理时间 0.623ms 左右
JVM 服务器发生了 2733 次 YGC(5647-2914),耗时 28.3 秒 (54.8-26.5),还有 20 次 FGC(77-57),耗时1.6 秒(2.4-1.8), GC总 耗时29.9秒(28.3+1.6)

2.5.2 1000 个并发用户/10 万请求量(总)
  • 执行ab -c 1000 -n 100000 http://127.0.0.1:8080/jvm/heap

在这里插入图片描述

在这里插入图片描述
测试结果:
用户的吞吐量为 1607/每秒左右
JVM 服务器平均请求处理时间 0.622ms 左右
JVM 服务器发生了 2746 次 YGC,耗时 28.2 秒 ,还有 18 次 FGC,耗时0.7 秒, GC总 耗时28.9秒。

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

2.6调优方案一

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

在这里插入图片描述

2.6.1 100 个并发用户/10 万请求量(总)
  • 执行ab -c 100 -n 100000 http://127.0.0.1:8080/jvm/heap

在这里插入图片描述

在这里插入图片描述
测试结果:
用户的吞吐量为 1826/每秒左右
JVM 服务器平均请求处理时间 0.548ms 左右
JVM 服务器发生了 940 次 YGC,耗时 20 秒 ,还有 7 次 FGC,耗时1.21 秒, GC总 耗时21.21秒。

2.6.2 1000 个并发用户/10 万请求量(总)
  • 执行ab -c 1000 -n 100000 http://127.0.0.1:8080/jvm/heap

在这里插入图片描述

在这里插入图片描述
测试结果:
用户的吞吐量为 1626/每秒左右
JVM 服务器平均请求处理时间 0.615ms 左右
JVM 服务器发生了 1030 次 YGC,耗时 21.2 秒 ,还有 17 次 FGC,耗时 2 秒, GC总耗时 23.2 秒。

2.7调优方案二

  • 调整堆内存空间减少 GC:通过分析,堆内存基本被用完了,而且存在大量 MinorGC 和 FullGC,这意味着我们的堆内存严重不足,这个时候我们需要调 大堆内存空间。
  • 堆空间加大到 1.5G,新生代1G,Eden区From区To区比例8:1:1
    java -jar -Xms1500m -Xmx1500m -Xmn1000m -XX:SurvivorRatio=8 jvm-1.0-SNAPSHOT.jar
    在这里插入图片描述
    在这里插入图片描述
2.7.1 100 个并发用户/10 万请求量(总)
  • 执行ab -c 100 -n 100000 http://127.0.0.1:8080/jvm/heap

在这里插入图片描述

在这里插入图片描述
测试结果:
用户的吞吐量为 2809/每秒左右
JVM 服务器平均请求处理时间 0.356ms 左右
JVM 服务器发生了 441 次 YGC,耗时 4.5 秒 ,还有 5 次 FGC,耗时 0.7 秒, GC总耗时 5.2 秒。

2.7.2 1000 个并发用户/10 万请求量(总)
  • 执行ab -c 1000 -n 100000 http://127.0.0.1:8080/jvm/heap

在这里插入图片描述

在这里插入图片描述
测试结果:
用户的吞吐量为 2425/每秒左右
JVM 服务器平均请求处理时间 0.412ms 左右
JVM 服务器发生了 491 次 YGC,耗时 8 秒 ,还有 8 次 FGC,耗时 1.6 秒, GC总耗时 9.6 秒。

2.8内存优化总结

在这里插入图片描述

  • 一般情况下,高并发业务场景中,需要一个比较大的堆空间,而默认参数情况下,堆空间不会很大。所以我们有必要进行调整。
  • 但是不要单纯的调整堆的总大小,要调整新生代和老年代的比例,以及 Eden 区还有 From 区,还有 To 区的比例。
  • 所以在我们上述的测试中,调整方案二,得到结果是最好的。在三种测试情况下都能够有非常好的性能指标,同时 GC 耗时相对控制也较好。
  • 对于调优方案一,就是单纯的加大堆空间,里面的比例不适合高并发场景,反而导致堆空间变大,没有明显减少 GC 的次数,但是每次 GC 需要检索对象的堆空间更大,反而可能会导致 GC 耗时更长。
  • 方案二:调整为一个很大的新生代和一个较小的老年代。这样可以尽可能回收掉大部分短期对象,减少中期的对象,而老年代尽存放长期存活对象。
  • 由于新生代空间较小,Eden 区很快被填满,就会导致频繁 Minor GC,因此我们可以通过增大新生代空间来降低 Minor GC 的频率。 单次 Minor GC 时间是由两部分组成:T1(扫描新生代)和 T2(复制存活对象)。

默认情况: 一个对象在 Eden 区的存活时间为 500ms,Minor GC 的时间间隔是 300ms,因为这个对象存活时间>间隔时间,那么正常情况下,Minor GC 的时间为 :T1+T2。
方案一: 整堆空间加大,但是新生代没有增大多少,对象在 Eden 区的存活时间为 500ms,Minor GC 的时间可能会扩大到 400ms,因为这个对象存活时间>间隔时间,那么正常情况下,Minor GC 的时间为 :T1x1.5(Eden 区加大了)+T2 。
方案二: 当我们增大新生代空间,Minor GC 的时间间隔可能会扩大到 600ms,此时一个存活 500ms 的对象就会在 Eden 区中被回收掉,此时就不存在复制存活对象了,所以再发生 Minor GC 的时间为:即 T1x2(Eden 区加大了)+T2x0。可见,扩容后,Minor GC 时增加了 T1,但省去了 T2 的时间。

在 JVM 中,复制对象的成本要远高于扫描成本。如果在堆内存中存在较多的长期存活的对象,此时增加年轻代空间,反而会增加 Minor GC 的时间。如 果堆中的短期对象很多,那么扩容新生代,单次 Minor GC 时间不会显著增加。因此,单次 Minor GC 时间更多取决于 GC 后存活对象的数量,而非 Eden 区的大小。

2.9推荐策略

1.新生代大小选择

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

2.老年代大小选择

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

3.GC优化

3.1GC性能衡量指标

  • 吞吐量:
    这里的衡量吞吐量是指应用程序所花费的时间和系统总运行时间的比值。我们可以按照这个公式来计算 GC 的吞吐量:系统总运行时间 = 应用程序耗时 +GC 耗时。如果系统运行了 100 分钟,GC 耗时 1 分钟,则系统吞吐量为 99%。GC 的吞吐量一般不能低于 95%。
  • 停顿时间:
    指垃圾回收器正在运行时,应用程序的暂停时间。对于串行回收器而言,停顿时间可能会比较长;而使用并发回收器,由于垃圾收集器和应用程序交替运行,程序的停顿时间就会变短,但其效率很可能不如独占垃圾收集器,系统的吞吐量也很可能会降低。
  • 垃圾回收频率:
    通常垃圾回收的频率越低越好,增大堆内存空间可以有效降低垃圾回收发生的频率,但同时也意味着堆积的回收对象越多,最终也会增加回收时的停顿 时间。所以我们需要适当地增大堆内存空间,保证正常的垃圾回收频率即可。

3.2分析 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 日志文件的输出路径
案例:
比如:导出前面测试案例中,默认情况下的 gc 日志
1、进行 1000 个并发用户/10 万请求量的压力测试,得到 gclogs 日志
java -jar -XX:+PrintGCDateStamps -XX:+PrintGCDetails -Xloggc:./gclogs jvm-1.0-SNAPSHOT.jar
2、进行 1000 个并发用户/10 万请求量的压力测试,得到 gc2logs 日志
java -jar -XX:+PrintGCDateStamps -XX:+PrintGCDetails -Xloggc:./gc2logs jvm-1.0-SNAPSHOT.jar

  • 使用日志工具 gcViewer
    这个工具的具体使用:见 https://github.com/chewiebug/GCViewer#readme

在这里插入图片描述

  • 明显第一个暂停总耗时比第二个要多很多,一个是 58 秒,一个是 15 秒左右,相差很多,这个本质上也可以分析出来,对于系统来说,第二个的 GC 日志 情况更加的好。

3.3GC 调优策略

  1. 降低 Minor GC 频率
    由于新生代空间较小,Eden 区很快被填满,就会导致频繁 Minor GC,因此我们可以通过增大新生代空间来降低 Minor GC 的频率。单次 Minor GC 时间是由两部分组成:T1(扫描新生代)和 T2(复制存活对象)。
    情况 1:假设一个对象在 Eden 区的存活时间为 500ms,Minor GC 的时间间隔是 300ms,因为这个对象存活时间>间隔时间,那么正常情况下,Minor GC 的时间为 :T1+T2。
    情况 2:当我们增大新生代空间,Minor GC 的时间间隔可能会扩大到 600ms,此时一个存活 500ms 的对象就会在 Eden 区中被回收掉,此时就不 存在复制存活对象了,所以再发生 Minor GC 的时间为:即 T1 x 2(空间大了)+T2 x 0
    可见,扩容后,Minor GC 时增加了 T1,但省去了 T2 的时间。
    在 JVM 中,复制对象的成本要远高于扫描成本。如果在堆内存中存在较多的长期存活的对象,此时增加年轻代空间,反而会增加 Minor GC 的时间。如果堆中的短期对象很多,那么扩容新生代,单次 Minor GC 时间不会显著增加。因此,单次 Minor GC 时间更多取决于 GC 后存活对象的数量,而非 Eden 区的大小。
    这个就解释了之前的内存调整方案中,方案一为什么性能还差些,但是到了方案二话,性能就有明显的上升。
  2. 降低 Full GC 的频率
    由于堆内存空间不足或老年代对象太多,会触发 Full GC,频繁的 Full GC 会带来上下文切换,增加系统的性能开销。
    减少创建大对象:在平常的业务场景中,我们一次性从数据库中查询出一个大对象用于 web 端显示。比如,一次性查询出 60 个字段的业务操作,这种大对象如果超过年轻代最大对象阈值,会被直接创建在老年代;即使被创建在了年轻代,由于年轻代的内存空间有限,通过 Minor GC 之后也会进入到老年代。这种大对象很容易产生较多的 Full GC。
    增大堆内存空间:在堆内存不足的情况下,增大堆内存空间,且设置初始化堆内存为最大堆内存,也可以降低 Full GC 的频率。
  3. 选择合适的 GC 回收器
    如果要求每次操作的响应时间必须在 500ms 以内。这个时候我们一般会选择响应速度较快的 GC 回收器,堆内存比较小的情况下(<6G)选择 CMS (Concurrent Mark Sweep)回收器和堆内存比较大的情况下(>8G)G1 回收器。
  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值