跳到主要内容

04、JVM 调优实战 - 性能调优之GC优化

今天分析 JVM 性能调优之GC优化,首先:

1、GC 优化 GC 性能衡量指标 吞吐量:

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

2、停顿时间:

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

3、垃圾回收频率:

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

4、分析 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 日志

java -jar -XX:+PrintGCDateStamps -XX:+PrintGCDetails -Xloggc:./gclogs jvm-1.0-SNAPSHOT.jar

1、 进行1000个并发用户/10万请求量的压力测试,得到gclogs日志;

 

java

-jar

-XX:+PrintGCDateStamps

-XX:+PrintGCDetails

-Xloggc:./gc2logs

-Xms1500m

-Xmx1500m

-Xmn1000m

-XX:SurvivorRatio=8

jvm-1.0-SNAPSHOT.jar

1、 进行1000个并发用户/10万请求量的压力测试,得到gc2logs日志;

 

使用日志工具 gcViewer

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

我们就暂停这项进行对比:

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

还有一个在线的工具 Gceasy https://gceasy.io/ 这个大家简单了解即可

GC 调优策略

5、降低 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*2 (空间大了) +T2*0

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

6、降低 Full GC 的频率

由于堆内存空间不足或老年代对象太多,会触发 Full GC ,频繁的 Full GC 会带来上下文切换,增加系统的性能开销。

7、减少创建大对象: 在平常的业务场景中,我们一次性从数据库中查询出一个大对象用于 web 端显示。比如,一次性查询出 60 个字段的业务操作,这种 大对象如果超过年轻代最大对象阈值,会被直接创建在老年代;即使被创建在了年轻代,由于年轻代的内存空间有限,通过 Minor GC 之后也会进入到老 年代。这种大对象很容易产生较多的 Full GC 。

8、增大堆内存空间: 在堆内存不足的情况下,增大堆内存空间,且设置初始化堆内存为最大堆内存,也可以降低 Full GC 的频率。

9、选择合适的 GC 回收器

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

10、总结

GC调优是个很复杂、很细致的过程,要根据实际情况调整,不同的机器、不同的应用、不同的性能要求调优的手段都是不同的,这些都需要大家平时去 积累,去观察,去实践。 算是抛砖引玉,一般调优的思路都是“ 测试 - 分析 - 调优 ” 三步走。

最后,给大家提个醒,任何调优都需要结合场景,明确已知问题和性能目标,不能为了调优而调优,以免引入新的 Bug ,带来风险和弊端。