本节将会介绍一下GC调优的目标,GC调优的准则,GC调优按照什么步骤进行,以及常用的Parallel GC、CMS GC、G1 GC的调优准则及调优案例。
目录
GC调优概述
GC调优的目标:
GC调优准则
GC调优步骤
Parallel收集器GC调优
CMS收集器GC调优
G1收集器GC调优
对JVM垃圾收集器进行调优之前,一定要先了解JVM内存结构、各个垃圾收集器的特点、常用的JVM参数、GC日志理解、GC日志可视化(GCeasy,GCViewer)等,因为这些东西不了解的话,是没法进行调优的。JVM调优是一个不断调整的过程,不能指望着一蹴而就。要不断调整相关参数,观察结果进行对比分析。还有就是,不同的垃圾收集器的JVM参数是不一样的,所以具体的GC调优要根据不同的收集器做调整。
推荐看下Java关于收集器调优的官方文档:https://docs.oracle.com/javase/8/docs/technotes/guides/vm/gctuning/toc.html,另外,还有一个plumbr系列博客也不错:https://plumbr.io/java-garbage-collection-handbook。
另外说一点,其实一般的Java程序是不需要调优的,除非你的应用程序并发访问量比较高。一般出现问题,都是由代码操作不当引起的,所以大部分情况下分析代码多过分析GC进行调优,我们在写代码的时候一定要注意优化,避免内存泄露。
根据官方文档中https://docs.oracle.com/javase/8/docs/technotes/guides/vm/gctuning/ergonomics.html关于对GC调优的目标翻译如下:
1、最大暂停时间目标(Maximum Pause Time Goal)
暂停时间是垃圾收集器停止应用程序并恢复不再使用的空间的持续时间。最大暂停时间目标的目的是限制这些暂停中的最长时间。垃圾回收器会维持平均的暂停时间和该平均值的方差。平均值是从执行开始时获取的,但经过加权后,最近的暂停次数会增加。如果平均时间加上暂停时间的方差大于最大暂停时间目标,则垃圾回收器认为未达到目标。
最大暂停时间目标是通过命令行选项指定的-XX:MaxGCPauseMillis=
2、吞吐量目标(Throughput Goal)
吞吐量目标是根据收集垃圾所花费的时间和垃圾收集之外所花费的时间(称为应用时间)来衡量的。目标由命令行选项指定-XX:GCTimeRatio=
垃圾收集所花费的时间是年轻代(young区)和老年代(Old区)收集的总时间。如果没有达到吞吐量目标,那么将增加世代的大小,以增加应用程序在集合之间运行的时间。
3、足迹目标(Footprint Goal)
如果已满足吞吐量和最大暂停时间目标,则垃圾收集器会减小堆的大小,直到无法满足其中一个目标(始终是吞吐量目标)。然后解决未实现的目标。
通俗来说,实现这两个目标:1.将转移到老年代的对象数量降低到最小;2.减少full GC的执行次数和时间。说一千道一万,GC调优的目的就是提高应用程序吞吐量、降低GC暂停时间。
根据官方文档中https://docs.oracle.com/javase/8/docs/technotes/guides/vm/gctuning/ergonomics.html关于对GC调优的目标翻译如下
简单来说如下所示:
ParallelGC调优准则
跟基本调优准则差不多
ParallelGC调优案例
示例使用的是一个springboot程序打的jar包
第一步:因为不设置收集器的情况下,默认是用的就是Parallel垃圾收集器,所以这里只是设置了gc log的参数,启动命令如下:
nohup java -XX:+PrintGCDetails -XX:+PrintGCTimeStamps -XX:+PrintGCDateStamps -Xloggc:./gc.log -jar demo.jar >/dev/null 2>&1&
启动完成后,将GC日志下载下来,用GCViewer打开如下:可以看到吞吐量为94.65%,young GC的次数为16次(13次是因为young区内存分配失败,3次是因为Metaspace导致的)、full GC的次数为3次(是因为Metaspace导致的)、GC最小停顿时间为0.00314s、最大停顿时间0.21881s、平均停顿时间0.02571s。
第二步:通过上面可以看到Metaspace导致了6次gc,通过上面第二个截图看到Metaspace占用了55.4M,那我们通过参数-XX:MetaspaceSize=64M 调大Metaspace的初始内存空间为64M,看下什么效果。启动命令如下:
nohup java -XX:MetaspaceSize=64M -XX:+PrintGCDetails -XX:+PrintGCTimeStamps -XX:+PrintGCDateStamps -Xloggc:./gc.log -jar demo.jar >/dev/null 2>&1&
同上面一样将GC日志用GCViewer打开如下:可以看到吞吐量为97.72%,young GC的次数为14次(因为young区内存分配失败)、full GC的次数为0次、GC最小停顿时间为0.00251s、最大停顿时间0.0361s、平均停顿时间0.0361s。
可以明显看到调大了Metaspace后,吞吐量上升了,fullgc没有了,gc总次数减少了,gc停顿时间也变小了。
第三步:我们通过上图看到堆内存增长到了724M,那我们尝试将最大堆内存设置为1024M,观察一下有什么效果(调优基本原则不建议设置,除非明确知道所需内存大小),启动命令如下:
nohup java -Xmx1024M -XX:MetaspaceSize=64M -XX:+PrintGCDetails -XX:+PrintGCTimeStamps -XX:+PrintGCDateStamps -Xloggc:./gc.log -jar demo.jar >/dev/null 2>&1&
然后继续重复上面步骤,打开如下:可以看到吞吐量达到了99.75%,已经很高了;GC次数还是14次,但是有一次Metaspace导致的full GC,说明上面将其设置64M可能太小了,可以自己试下调大点看看效果;GC停顿时间要比第二步中变长了。
第四步,可以设置一个吞吐量(99%),gc停顿时间(100毫秒)的目标,启动命令如下:
nohup java -Xms1024M -Xmx1024M -XX:MetaspaceSize=64M -XX:MaxGCPauseMillis=100 -XX:GCTimeRatio=99 -XX:+PrintGCDetails -XX:+PrintGCTimeStamps -XX:+PrintGCDateStamps -Xloggc:./gc.log -jar hgwd-usercenter.jar >/dev/null 2>&1&
可以看到如下结果:吞吐量比第三步下降了,但是由于我们设置了最大停顿时间100毫秒,我们看到GC停顿的最小、最大、平均停顿时间都比第三步减小了。
另外,我们发现GC日志中基本上都是young gc,那么如果调高young区每次的增长量(默认20%)效果怎么样呢?我们可以通过JVM参数 XX:YoungGenerationSizeIncrement=30,将其改为30%,然后启动重新按照上面步骤分析GC日志观察效果如何,这里我就不演示了,大家可以自己试一下。
CMS 收集器常用参数
CMS收集器调优要根据其工作过程原理,常用有如下参数:
XX:+CMSParallelInitialMarkEnabled设置初始标记阶段并发执行,JDK1.8之前初始标记都是单线程的。
可终止的预清理需要设置一个时间,CMS提供了一个参数CMSMaxAbortablePrecleanTime ,默认为5S。只要到了5S,不管发没发生Minor GC,有没有到CMSScheduleRemardEdenPenetration都会中止此阶段,进入remark(重新标记)阶段。
如果在5S内还是没有执行Minor GC怎么办?CMS提供CMSScavengeBeforeRemark参数,使remark前强制进行一次Minor GC。这样做好的一面是减少了remark阶段的停顿时间,坏的一面是Minor GC后紧跟着一个remark pause。如此一来,停顿时间也比较久。
CMS GC调优案例
类似Parallel收集器的调优步骤,对相应的参数进行调整,观察关键指标:吞吐量、GC次数、最小停顿时间、最大停顿时间、平均停顿时间等,然后进行调优。
G1 GC调优准则
年轻代大小:避免使用-Xmn, -XX:NewRatio 等显式 设置Young 区大小,固定年轻一代的大小会覆盖目标暂停时间目标
注意:G1收集器没有full GC,而是Mixed GC,Mixed GC会回收young 区和部分old区。
G1关于Mixed GC调优常用参数:
其他常用参数:
G1 GC调优案例
类似上面步骤。