在某些情况下,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] |
第三阶段:并发标记
这个阶段和CMS并发标记非常相似:遍历整个堆里的对象图,找到要回收的对象,此阶段与应用程序线程同时运行,并发标记时会产生漏标、错标问题,G1使用SATB算法来解决。
1.364: [GC concurrent-mark-start] |
第四阶段:最终标记
此阶段会stop-the-world暂停,与CMS一样,完成最终的标记。对用户程序线程做短暂的暂停,用于处理并发标记阶段遗留下的SATB记录。
第五阶段:筛选回收
此阶段会计算堆中所有的活动对象,根据GC效率对这些区域排序,并按照-XX:MaxGCPauseMillis参数设定的毫秒数对价值最高的区域进行回收。这个阶段某部分是需要stop-the-world暂停的,例如标记初始标记以来所有对象的卡位图(TASM之上的所有对象)、为任何具有一个活动对象的区域标记区域位图、清理没有活动对象的区域RSet集等。
1.872: [GC cleanup 1357M->173M(1996M), 0.0015664 secs] |
某部分是并发的,例如空区域回收和大部分活跃对象计算等。
1.874: [GC concurrent-cleanup-start] |
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/16JVM会根据实际情况决定到底是乘以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 |
-Xmn或-XX:NewSize和-XX:MaxNewSize
G1会根据应用程序的行为动态的调整年轻代。设置这些选项会导致错误的年轻代优化,应该尽量避免设置这些。