在某些情况下,CMS GC的性能比G1更高,因此一些人推荐在Java8坚持使用CMS,而由于CMS在Java9被启用,切换到另一个GC迫在眉睫,因此转向G1是目前最好的解决方案。

G1基础

G1关键设计的目标之一是使垃圾收集导致的stop-the-world暂停持续时间变得可预测和可配置。实际上,G1是一个软实时的垃圾收集器,简单说就是您可以为stop-the-world配置一个毫秒长的时间,G1 GC将尽最大可能实现这个目标。

为实现stop-the-world的时间可预测,G1将堆分成多个(通常大约2048个)可以容纳对象、大小相等的独立区域(Region),可以通过 -XX:G1HeapRegionSize 配置每个Region大小,每个Region大小可以从1MB到32MB不等,并且必须是 2 的幂。

每个Region可能是Eden,也可能是Survivor,也可能是Old,所有的Eden和Survivor区逻辑上组成年轻代,所有Old区逻辑上组成老年代。另外Region还有一类特殊的Humongous区域,专门用来存储大对象。G1认为只要大小超过了一个Region容量一半的对象都被认定为大对象,对于那些超过了整个Region容量的超级大对象,将被存放在多个连续的Humongous Region中。G1进行垃圾回收时,会将Humongous当成老年代回收。

stop-the-world

在垃圾收集期间,有许多事件使整个应用程序暂停,这个被称为stop-the-world事件。在stop-the-world期间,对JVM的任何请求都被暂停。因此,GC调优目标之一就是尽量减少stop-the-world持续时间。Full GC通常stop-the-world暂停时间最长,因为要遍历整个堆,而其他stw暂停可能更短或者在可接受的范围之内。在GC调优过程中,GC日志分析有助于了解暂停的时间以及原因,从而采取纠正措施。

G1 GC

G1 GC有3个主要集合:

  • Young GC:只清理年轻代,即活动对象从Eden移到Survivor,从一个Survivor移到另一个Survivor,以及达到MaxTenuringThreshold年龄的对象移到Old区。
  • Mixed GC:年轻代和老年代中包含垃圾最多的一些区域(可配置)也被清除,这也是Garbage First的名字,即垃圾优先。
  • Full GC:当Mixed GC失败,堆中没有足够的空间给复制对象使用,触发单线程的stop-the-world暂停使用“标记整理”算法进行垃圾收集。

Young GC

当JVM分配对象到Eden区域失败时,便会触发stop-the-world暂停多线程并行来进行年轻代的垃圾收集,YGC 将 Eden Region 中存活的对象拷贝到Survivor,或者直接晋升到Old Region中;将Survivor Regin中存活的对象拷贝到新的Survivor或者晋升Old Region。

Mixed GC

G1收集器很多理念是建立在CMS概念之上。G1并发标记使用Snapshot-At-The-Beginning(SATB或原始快照)方法标记周期开始时处于活动状态的对象,而CMS则采用增量更新。

当堆的整体占用足够大时,并发标记开始,默认情况下是45%,也可以通过设置 -XX:InitiatingHeapOccupancyPercent 参数进行修改,跟CMS类似,G1的并发标记由许多阶段组成,其中一些是与应用程序线程完全并发的,而另一些则需要停止应用程序线程。

第一阶段:初始标记

此阶段标记所有可从GC根直接访问的对象。它需要stop-the-world暂停。可以从日志 Evacuation Pause的“initial-mark”来查看:

1.631: [GC pause (G1 Evacuation Pause) (young) (initial-mark), 0.0062656 secs]

第二阶段:根区域扫描

这个阶段标记所有可从根区域访问的活动对象,即那些不为空的对象。此阶段与应用程序线程同时运行。

1.362: [GC concurrent-root-region-scan-start]
1.364: [GC concurrent-root-region-scan-end, 0.0028513 secs]

第三阶段:并发标记

这个阶段和CMS并发标记非常相似:遍历整个堆里的对象图,找到要回收的对象,此阶段与应用程序线程同时运行,并发标记时会产生漏标、错标问题,G1使用SATB算法来解决。

1.364: [GC concurrent-mark-start]
1.645: [GC concurrent-mark-end, 0.2803470 secs]

第四阶段:最终标记

此阶段会stop-the-world暂停,与CMS一样,完成最终的标记。对用户程序线程做短暂的暂停,用于处理并发标记阶段遗留下的SATB记录。

第五阶段:筛选回收

此阶段会计算堆中所有的活动对象,根据GC效率对这些区域排序,并按照-XX:MaxGCPauseMillis参数设定的毫秒数对价值最高的区域进行回收。这个阶段某部分是需要stop-the-world暂停的,例如标记初始标记以来所有对象的卡位图(TASM之上的所有对象)、为任何具有一个活动对象的区域标记区域位图、清理没有活动对象的区域RSet集等。

1.872: [GC cleanup 1357M->173M(1996M), 0.0015664 secs]
[Times: user=0.01 sys=0.00, real=0.01 secs]

某部分是并发的,例如空区域回收和大部分活跃对象计算等。

1.874: [GC concurrent-cleanup-start]
1.876: [GC concurrent-cleanup-end, 0.0014846 secs]

Full GC

如果Mixed GC在进行GC回收拷贝对象时,没有足够的空Region能够承载拷贝对象就会触发Full GC。Full GC是单线程的stop-the-world暂停,使用“标记整理”算法进行垃圾收集,这个过程是非常耗时的。

G1 GC调优

G1 GC的可用JVM选项比CMS少得多,为G1 GC调整jVM的基本策略是设置堆大小和暂停时间,然后让JVM动态修改所需的设置以尝试满足暂停时间目标。

基本JVM选项

使用G1垃圾收集器

在Java8上,默认GC是Parallel GC,而在Java11上默认是G1 GC。

-XX:+UseG1GC

堆大小

建议将最小和最大堆设置相同值,从而避免在应用程序生命周期中堆的动态收缩和增长。

-XX:InitialHeapSize (最小堆大小)  -XX:MaxHeapSize (最大堆大小)

-Xms和-Xmx选项是上述选项的快捷方式,因此选用任一搭配都可以。

暂停目标和年轻代大小

G1 GC有一个试图满足的暂停时间目标,即软目标。

在年轻代收集期间,G1 GC会调整年轻代的大小以满足这个目标。出于这个原因,建议设置暂停时间,让GC根据需要自己更改年轻代大小。

特别注意:除非需要,否则不要设置年轻代大小

为了设置暂停时间目标,请设置以下JVM选项:

-XX:MaxGCPauseMillis

例如,您可以首先将此值设置在200-500之间(单位毫秒)并测试是否满足您的性能要求,默认200ms

垃圾收集记录

调优是基于收集前后数据进行对比的迭代过程,因此开启GC日志记录尤为重要,即使在生产环境也是如此。显然需要一个日志记录策略来处理系统上消耗资源的日志。默认日志级别为info。

建议设置以下JVM选项:

  • -Xlog - 这将使用指定选项进行设置
  • gc* - 打印所有GC事件
  • safepoint - 打印先前使用Java8设置的值
  • age* - 在调试级别打印详细信息
  • ergo* - 对大多数信息使用调式级别
  • time - ISO-8601格式的当前时间和日期
  • level - 与日志消息相关的级别
  • tags - 与日志消息关联的标签集
  • uptime - 自JVM启动以来的时间
  • file=filename - 文件名,可选择包含%p或/或%t已包含JVM的PID和启动时间戳
  • filesize=size - 设置文件大小

上面例子:

-Xlog:gc*,safepoint,age*,ergo*:file=/opt/gclogs/gc-%p-%t.log:tags,uptime,time,level:filecount=10,filesize=50m

其他JVM选项

JVM选项 描述
-XX:DisableExplicitGC 建议设置此值以禁用对System.gc()方法调用
-XX:+UseStringDeduplication 字符串重复数据删除减少Java堆上String对象的内存占用。默认情况下禁用此功能
-XX:MaxMetaspaceSize= 设置元数据分配的本机最大内存。建议将此值设置为256MB
-XX:MaxTenuringThreshold= 设置最大对象在年轻代存活年龄阈值。默认为15,对于大部分应用服务器,保持默认即可。
-XX:+ParallelRefProcEnabled 建议设置此值以启用并行引用处理。默认情况下,此项处于禁用状态。

进一步调整

以下选项没有具体的推荐值,需要基于您的应用程序自行分析设置。

-XX:ParallelGCThreads

指定并行GC线程数,也就是STW期间工作的GC线程数,遵循以下原则:

  • 如果用户指定该值,使用用户指定的值

  • 用户未指定时,如果CPU逻辑核数小于8,则ParallelGCThreads为CPU逻辑核数

  • 用户未指定,如果CPU逻辑核数大于8,则计算方式为:

    ParallelGCThreads=8 + (N - 8) * 5/8

    ParallelGCThreads=8 + (N - 8) * 5/16

    JVM会根据实际情况决定到底是乘以5/8还是5/16。

-XX:ConcGCThreads

设置并行标记线程的数量。默认情况下设置为并行垃圾收集线程数(ParallelGCThreads)的1/4。例如,具有16个逻辑处理器的系统将默认ParallelGCThreads为16,因此ConcGCThreads为4。您可以增加ConcGCThreads以增加并行标记线程的数量并减少标记期间的暂停时间。

-XX:ConcGCThreads=n

-XX:InitiatingHeapOccupancyPercent

该值决定初始标记何时开始,默认为45%。但G1 GC尝试为IHOP寻找最佳值,并且仅在以下情况使用该值:没有足够的信息进行优化或自适应IHOP被覆盖。

例如,如果由于分配失败而获得Full GC,或者在GC日志中看到Evacuation Pause/Evacuation Failure,这通常意味着无法分配对象,因为没有足够的内存或无法足够快地回收对象,这时您可以通过降低IHOP值提前进行初始标记。为了覆盖自适应行为,您可以设置该 -XX:G1UseAdaptiveIHOP 选项。

-XX:InitiatingHeapOccupancyPercent

-XX:G1MixedGCLiveThresholdPercent

通过设置该值指定被纳入CSet Region地存活空间占比阈值。不同版本默认值不同,有65%和85%。在全局地并发标记阶段,如果一个Region地存活对象空间占比超过该值,那么就会被纳入CSet。该值会直接影响到Mixed GC选择回收地区域。当GC时间较长时,表示GC回收的空间较大,那么可以尝试调低该值,但设置不合理也可能导致垃圾回收不彻底,最终导致Full GC。

-XX:G1MixedGCLiveThresholdPercent=85

-XX:G1HeapWastePercent

通过设置该值指定触发Mixed GC的堆垃圾占比,默认值为5%。在全局标记结束后统计出所有CSet内可被回收的垃圾占整个堆的占比,如果超过5%,那么就会触发之后的Mixed GC,如果不超过,那么在之后的某次Young GC中重新执行全局并发标记。可以适当调高此阈值,能够适当降低Mixed GC的频率。

-XX:G1HeapWastePercent=5

-XX:G1HeapRegionSize

设置Region大小。默认G1将整个堆分成大约2048个Region,每块大小需要为2的幂次方。Region的大小主要关系到Humongous的判断,当一个对象超过Region大小的一半时,则为巨大对象,那么其会至少独占一个Region,如果一个放不下,会占用连续的多个Region。如果一个Humongous Region放入了巨大对象,可能还有其他空间,但是剩余空间不能用于存放其他对象,就造成了空间浪费。如果你的java应用里有很多大小差不多的巨大对象,可以适当调大Region的大小。

-XX:G1HeapRegionSize=2M

-XX:G1NewSizePercent和-XX:G1MaxNewSizePercent

年轻代比例有两个数值指定。-XX:G1NewSizePercent指定年轻代占整个堆的下限占比,默认为5%,-XX:G1MaxNewSizePercent指定年轻代占整个堆的上限占比,默认为60%。G1会根据实际GC情况(STW时间)来动态调整年轻代的大小。需要平衡年轻代和老年代的空间。

-XX:G1NewSizePercent=5
-XX:G1MaxNewSizePercent=60

-Xmn或-XX:NewSize和-XX:MaxNewSize

G1会根据应用程序的行为动态的调整年轻代。设置这些选项会导致错误的年轻代优化,应该尽量避免设置这些。