本节将会介绍一下GC调优的目标,GC调优的准则,GC调优按照什么步骤进行,以及常用的Parallel GC、CMS GC、G1 GC的调优准则及调优案例。
目录
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系列博客也不错:Java Garbage Collection handbook | Plumbr – User Experience & Application Performance Monitoring。
另外说一点,其实一般的Java程序是不需要调优的,除非你的应用程序并发访问量比较高。一般出现问题,都是由代码操作不当引起的,所以大部分情况下分析代码多过分析GC进行调优,我们在写代码的时候一定要注意优化,避免内存泄露。
GC调优的目标:
根据官方文档中https://docs.oracle.com/javase/8/docs/technotes/guides/vm/gctuning/ergonomics.html关于对GC调优的目标翻译如下:
1、最大暂停时间目标(Maximum Pause Time Goal)
暂停时间是垃圾收集器停止应用程序并恢复不再使用的空间的持续时间。最大暂停时间目标的目的是限制这些暂停中的最长时间。垃圾回收器会维持平均的暂停时间和该平均值的方差。平均值是从执行开始时获取的,但经过加权后,最近的暂停次数会增加。如果平均时间加上暂停时间的方差大于最大暂停时间目标,则垃圾回收器认为未达到目标。
最大暂停时间目标是通过命令行选项指定的-XX:MaxGCPauseMillis=<nnn>。这被解释为垃圾收集器的提示,要求暂停时间为<nnn>毫秒或更短。垃圾收集器将调整Java堆大小和与垃圾收集相关的其他参数,以使垃圾收集暂停时间短于<nnn>毫秒。默认情况下,没有最大暂停时间目标。这些调整可能导致垃圾回收器更频繁地发生,从而降低了应用程序的整体吞吐量。垃圾收集器会尝试在吞吐量目标之前达到任何暂停时间目标。但是,在某些情况下,无法达到所需的暂停时间目标。
2、吞吐量目标(Throughput Goal)
吞吐量目标是根据收集垃圾所花费的时间和垃圾收集之外所花费的时间(称为应用时间)来衡量的。目标由命令行选项指定-XX:GCTimeRatio=<nnn>。垃圾收集时间与应用程序时间的比率为1 /(1 + <nnn>)。例如,-XX:GCTimeRatio=19将垃圾收集目标设置为目标1/20或总时间的5%。
垃圾收集所花费的时间是年轻代(young区)和老年代(Old区)收集的总时间。如果没有达到吞吐量目标,那么将增加世代的大小,以增加应用程序在集合之间运行的时间。
3、足迹目标(Footprint Goal)
如果已满足吞吐量和最大暂停时间目标,则垃圾收集器会减小堆的大小,直到无法满足其中一个目标(始终是吞吐量目标)。然后解决未实现的目标。
通俗来说,实现这两个目标:1.将转移到老年代的对象数量降低到最小;2.减少full GC的执行次数和时间。说一千道一万,GC调优的目的就是提高应用程序吞吐量、降低GC暂停时间。
GC调优准则
根据官方文档中https://docs.oracle.com/javase/8/docs/technotes/guides/vm/gctuning/ergonomics.html关于对GC调优的目标翻译如下
- 除非您知道需要的堆大于默认的最大堆大小,否则不要为堆选择最大值。
- 如果堆增长到最大大小,并且无法满足吞吐量目标,则最大堆大小对于吞吐量目标而言太小。将最大堆大小设置为接近平台上总物理内存(不能让OS使用swap内存)。再次执行该应用程序。如果仍然没有达到吞吐量目标,那么对于平台上的可用内存来说,应用程序时间目标太高了。
- 如果可以满足吞吐量目标,但暂停时间太长,则选择最大暂停时间目标。选择最大暂停时间目标可能意味着您的吞吐量目标将无法实现,因此请选择对应用程序可接受的折衷值。
简单来说如下所示:
- 除非确定,否则不要设置最大堆内存
- 如果吞吐量目标达不到,则调大最大内存,不能让OS使用swap。如果还达不到,则降低目标。
- 吞吐量可以达到,GC暂停时间太长,则设置暂停时间的目标。
GC调优步骤
- 打印GC日志:前面的博客《GC日志介绍》中已经介绍了GC日志的打印及分析
- 根据GC日志得到关键性能指标:将上面得到的GC日志通过GC日志可视化分析工具GCeasy和GCViewer,得到吞吐量、暂停时间、GC次数等关键指标,可以参考《GC日志可视化分析工具GCeasy和GCViewer》
- 分析GC原因,调整JVM参数:查看GC的次数和原因等,是young GC,还是Full GC,什么原因引起的young GC、Full GC。分析之后,根据选用的垃圾收集器调整相关的参数,重复上面步骤,观察调优结果。
Parallel收集器GC调优
ParallelGC调优准则
跟基本调优准则差不多
- 除非确定,否则不要设置最大堆内存
- 优先设置吞吐量目标
- 如果吞吐量目标达不到,则调大最大内存,不能让OS使用swap。如果还达不到,则降低目标。
- 吞吐量可以达到,GC暂停时间太长,则设置暂停时间的目标。
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收集器GC调优
CMS 收集器常用参数
CMS收集器调优要根据其工作过程原理,常用有如下参数:
-
XX:+CMSParallelInitialMarkEnabled设置初始标记阶段并发执行,JDK1.8之前初始标记都是单线程的。
- CMS并发预清理(CMS concurrent preclean)阶段有两个参数:CMSScheduleRemarkEdenSizeThreshold、CMSScheduleRemarkEdenPenetration,默认值分别是2M、50%。两个参数组合起来的意思是并发预清理后,eden空间使用超过2M时启动可中断的并发预清理(CMS-concurrent-abortable-preclean),直到eden空间使用率达到50%时中断,进入remark(重新标记)阶段。
可终止的预清理需要设置一个时间,CMS提供了一个参数CMSMaxAbortablePrecleanTime ,默认为5S。只要到了5S,不管发没发生Minor GC,有没有到CMSScheduleRemardEdenPenetration都会中止此阶段,进入remark(重新标记)阶段。
如果在5S内还是没有执行Minor GC怎么办?CMS提供CMSScavengeBeforeRemark参数,使remark前强制进行一次Minor GC。这样做好的一面是减少了remark阶段的停顿时间,坏的一面是Minor GC后紧跟着一个remark pause。如此一来,停顿时间也比较久。
- -XX:CMSInitiatingOccupancyFraction设置触发GC的百分比(在JDK1.6中默认为92%),要是CMS运行期间预留的内存无法满足程序需要,就会出现一次“Concurrent Mode Failure”失败,这时虚拟机将启动后备预案:临时启用SenalOld收集器来重新进行老年代的垃圾收集,停顿时间就会变长了。
- -XX:+UseCMSCompactAtFullCollection开关参数(默认开启),用于开启内存碎片整理。 -XX:CMSFullGCsBeforeCompaction,用于设置执行多少次不压缩的Full GC后,跟着来一次带压缩的(默认值为0,表示每次进入FullGC时都进行碎片整理)。
- -XX:ConcGCThreads:设置并发垃圾收集的线程数,默认该值是基于ParallelGCThreads计算出来的;
- -XX:+UseCMSInitiatingOccupancyOnly:是否动态可调,用这个参数可以使CMS一直按CMSInitiatingOccupancyFraction设定的值启动
- -XX:+CMSScavengeBeforeRemark:强制hotspot虚拟机在cms remark阶段之前做一次minor gc,用于提高remark阶段的速度;
- -XX:+CMSClassUnloadingEnable:如果有的话,启用回收Perm 区(JDK8之前)
- -XX:+CMSParallelInitialEnabled, 用于开启CMS initial-mark阶段采用多线程的方式进行标记,用于提高标记速度,在Java8开始已经默认开启;
- -XX:+CMSParallelRemarkEnabled,用户开启CMS remark阶段采用多线程的方式进行重新标记,默认开启;
- -XX:+CMSPrecleaningEnabled,指定CMS是否需要进行Pre cleaning这个阶段
CMS GC调优案例
类似Parallel收集器的调优步骤,对相应的参数进行调整,观察关键指标:吞吐量、GC次数、最小停顿时间、最大停顿时间、平均停顿时间等,然后进行调优。
G1收集器GC调优
官网G1GC调优指南:https://docs.oracle.com/javase/8/docs/technotes/guides/vm/gctuning/g1_gc_tuning.html#recommendations
G1 GC调优准则
-
年轻代大小:避免使用-Xmn, -XX:NewRatio 等显式 设置Young 区大小,固定年轻一代的大小会覆盖目标暂停时间目标
- 暂停时间目标:暂停时间不要太严苛,G1 GC吞吐量目标是90%的应用程序时间和10%的垃圾回收时间,太严苛会直接影响到吞吐量
注意:G1收集器没有full GC,而是Mixed GC,Mixed GC会回收young 区和部分old区。
G1关于Mixed GC调优常用参数:
- -XX:InitiatingHeapOccupancyPercent:设置堆占用率的百分比(0到100)达到这个数值的时候触发global concurrent marking(全局并发标记),默认为45%。值为0表示间断进行全局并发标记。
- -XX:G1MixedGCLiveThresholdPercent:设置Old区的region被回收时候的对象占比,默认占用率为85%。只有Old区的region中存活的对象占用达到了这个百分比,才会在Mixed GC中被回收。
- -XX:G1HeapWastePercent:在global concurrent marking(全局并发标记)结束之后,可以知道所有的区有多少空间要被回收,在每次young GC之后和再次发生Mixed GC之前,会检查垃圾占比是否达到此参数,只有达到了,下次才会发生Mixed GC.
- -XX:G1MixedGCCountTarget:一次global concurrent marking(全局并发标记)之后,最多执行Mexed GC的次数,默认是8。
- -XX:G1OldCSetRegionThresholdPercent:设置Mixed GC收集周期中要收集的Old region数的上限。默认值是Java堆的10%
其他常用参数:
- -XX:+UseG1GC 开启 G1
- -XX:G1HeapRegionSize=n, 设置每个Region 的大小,该值将是2的幂,范围1-32M,最多2048个region
- -XX:MaxGCPauseMillis=200 最大停顿时间
- -XX:G1NewSizePercent、-XX:G1MaxNewSizePercent:新生代占用整个堆内存的最小百分比(默认5%)、最大百分比(默认60%)
- -XX:G1ReservePercent=10 保留内存区域,防止 to space(Survivor中的to区)溢出
- -XX:ParallelGCThreads=n SWT线程数
- -XX:ConcGCThreads=n 并发线程数=1/4*并行
G1 GC调优案例
类似上面步骤。