JVM垃圾回收调优策略

对于不同的业务场景,垃圾回收调优策略是不一样的。比如,在对内存要求苛刻的情况下,需要提高对象回收效率;在CPU使用率高的情况下,需要降低高并发时垃圾回收的频率。那么面对不同的场景,我们该如何进行调优呢?

一、GC性能衡量指标

在进行实际的GC调优前,我们需要根据一些GC性能指标,确定具体的调优方向。常用的GC性能指标如下:

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

二、查看 & 分析GC日志

在了解了性能衡量指标后,我们需要通过工具分析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,日志文件的输出路径

一般我们使用如下参数来输出日志:

1
-XX:+PrintGCDateStamps -XX:+PrintGCDetails -Xloggc:./gclogs

对于运行很短时间的GC日志,我们可直接查看日志文件。但如果是长时间的GC日志,我们很难通过文本形式去查看整体的GC性能。

此时,我们可以使用GCViewer工具打开日志文件,通过图形化界面查看整体的GC性能。通过工具,我们可以得到吞吐量、停顿时间以及GC频率等指标数据,从而可以非常直观地了解到GC的性能情况。

其实,还有一个比较好用的GC日志分析工具,GCeasy是一款非常直观地GC日志分析工具,我们可以将日志文件压缩之后,上传到GCeasy官网即可看到非常清楚的GC日志分析结果。

三、GC调优策略

3.1、选择合适的垃圾回收器

假设我们有这样一个需求,要求每次GC操作的响应时间必须在500ms以内。这时,我们一般会选择响应速度比较快的垃圾回收器,比如,CMS回收器和G1回收器。

而当我们的需求对系统吞吐量有要求时,就可以选择Parallel Scavenge回收器来提高系统的吞吐量。

3.2、降低Minor GC频率

通常情况下,由于新生代空间比较小,Eden区很快就会被填满,此时就会导致频率的Minor GC。因此,我们可以通过增大新生代空间来降低Minor GC的频率。此时,我们可能会有这样的疑问:

扩容Eden区虽然可以减少Minor GC的次数,但不会增加单次Minor GC的时间吗?如果单次Minor GC的时间增加,那也很难达到我们期待的优化效果呀。

我们需要知道的是,单次Minor GC的时间是由两部分组成的:T1(扫描新生代)和T2(复制存活对象)。假设一个对象在Eden区的存活时间为500ms,Minor GC的时间间隔是300ms,那么正常情况下,Minor GC的时间为:T1+T2。

当我们增大新生代空间,Minor GC的时间间隔可能会扩大到600ms,此时一个存活500ms的对象就会在Eden区中被回收,也就不存在复制存活对象了。所以,再发生Minor GC的时间为:两次扫描新生代,即2T1。

可见,扩容后,Minor GC时增加了T1,但省去T2。通常在虚拟机中,复制对象的成本要远高于扫面成本。

需要注意的是,如果在堆内存中存在比较多的长期存活对象,此时增加新生代空间,反而会增加Minor GC的时间。如果堆中的短期对象很多,那么扩容新生代,单次Minor GC的时间不会显著增加。因此,单次Minor GC的时间更多取决于GC后存活对象的数量,而非Eden区的大小。

3.3、降低Full GC频率

通常情况下,在堆内存空间不足或老年代对象太多时,会触发Full GC,频繁的Full GC会带来上下文切换,增加系统的性能开销。那我们可以使用哪些方法来降低Full GC的频率呢?

  • 减少创建大对象。在平常的业务场景中,我们习惯一次性从数据库中查询出一个大对象用于Web端显示。例如,一次性查询出60个字段的业务操作,这种大对象如果超过新生代最大对象阈值,会被直接创建在老年代;即时被创建在了新生代,由于新生代的内存空间有限,通过Minor GC之后也会进入到老年代。这种大对象很容易产生较多的Full GC。我们可以将这种大对象拆解出来,首次只查询一些比较重要的字段,如果还需要其它字段辅助查看,则再通过第二次查询显示剩余的字段。
  • 增大堆内存空间。在堆内存不足的情况下,增大堆内存空间,且设置初始化堆内存为最大堆内存,也可以降低Full GC的频率。

四、小结

在这篇文章中,我主要讨论了如下概念:

  • 吞吐量、停顿时间和GC频率是衡量GC性能的常用指标。
  • 通过分析GC日志可以获取到GC性能指标数据。而常用的GC日志分析工具有GCViewer和GCeasy。
  • 选择合适的垃圾回收器、降低Minor GC频率、降低Full GC频率是常用的GC调优策略。

JVM垃圾回收调优策略
https://kuberxy.github.io/2020/09/26/JVM垃圾回收调优策略/
作者
Mr.x
发布于
2020年9月26日
许可协议