跳到主要内容

03、JVM 调优实战 - 性能调优之内存优化

今天我们进行JVM 性能优化:

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

1、压测工具 AB

Ab(ApacheBench) 测试工具是 Apache 提供的一款测试工具,具有简单易上手的特点,在测试 Web 服务时非常实用。

ab一般都是在 Linux 上用。 安装非常简单,只需要在 Linux 系统中输入 yum-y install httpd-tools 命令,就可以了。

安装成功后,输入 ab 命令,可以看到以下信息:

 

ab工具用来测试 post get 接口请求非常便捷,可以通过参数指定请求数、并发数、请求参数等

测试get 请求接口 ab -c 10 -n 100 http://www.test.api.com/test/login?userName=test&password=test

测试post 请求接口

ab-n 100 -c 10 -p 'post.txt' -T 'application/x-www-form-urlencoded' 'http://test.api.com/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 );

输出中,性能指标参考

 

Requests per second :吞吐率,指某个并发用户数下单位时间内处理的请求数;

Time per request :上面的是用户平均请求等待时间,指处理完成所有请求数所花费的时间 / (总请求数 / 并发用户数);

Time per request :下面的是服务器平均请求处理时间,指处理完成所有请求数所花费的时间 / 总请求数;

Percentage of the requests served within a certain time :每秒请求时间分布情况,指在整个请求中,每个请求的时间长度的分布情况,例如有 50% 的请求

响应在 8ms 内, 66% 的请求响应在 10ms 内,说明有 16% 的请求在 8ms~10ms 之间。

2、JVM 堆内存分配

JVM 内存分配的调优案例

一个高并发系统中的抢购接口,高峰时 5W 的并发请求,且每次请求会产生 20KB 对象(包括订单、用户、优惠券等对象数据)。

我们可以通过一个并发创建一个 1MB 对象的接口来模拟万级并发请求产生大量对象的场景,具体代码如下:

3、AB 压测

对应用服务进行压力测试,模拟不同并发用户数下的服务的响应情况:

1、 10个并发用户/10万请求量(总);

2、 100个并发用户/10万请求量(总);

3、 1000个并发用户/10万请求量(总);

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

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

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

4、服务器信息

我本机起一台 Linux 虚拟机,分配的内存为 2G ,处理器数量为 2 个。具体信息如下图:

5、GC 监控

还有一句话,无监控不调优,所以我们需要监控起来。 JVM 中我们使用 jstat 命令监控一下 JVM 的 GC 情况。

统计GC 的情况。

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

 

6、堆空间监控

在默认不配置 JVM 堆内存大小的情况下, JVM 根据默认值来配置当前内存大小。

我们可以通过以下命令来查看堆内存配置的默认值:

java -XX:+PrintFlagsFinal -version | grep HeapSize

 

7、调整方案二

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

使用AB 进行压力测试:

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

 

内存优化总结:

 

 

一般情况下,高并发业务场景中,需要一个比较大的堆空间,而默认参数情况下,堆空间不会很大。所以我们有必要进行调整。

但是不要单纯的调整堆的总大小,要调整新生代和老年代的比例,以及 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 的时间为 : T1*1.5 ( Eden 区加大了) +T2

方案二: 当我们增大新生代空间, Minor GC 的时间间隔可能会扩大到 600ms ,此时一个存活 500ms 的对象就会在 Eden 区中被回收掉,此时就不

存在复制存活对象了,所以再发生 Minor GC 的时间为:即 T1*2 (空间大了) +T2*0

可见,扩容后, Minor GC 时增加了 T1 ,但省去了 T2 的时间。

在JVM 中,复制对象的成本要远高于扫描成本。如果在堆内存中存在较多的长期存活的对象,此时增加年轻代空间,反而会增加 Minor GC 的时间。如

果堆中的短期对象很多,那么扩容新生代,单次 Minor GC 时间不会显著增加。因此,单次 Minor GC 时间更多取决于 GC 后存活对象的数量,而非 Eden

区的大小。

这个就解释了之前的内存调整方案中,方案一为什么性能还差些,但是到了方案二话,性能就有明显的上升。

8、推荐策略

(1)新生代大小选择

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

(2)老年代大小选择

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

到此,JVM 性能调优之内存优化分析结束!