对G1算法和G1垃圾回收器的理解

G1(Garbage-First )垃圾回收算法,HotspotVM的四种GC算法之一。
G1垃圾回收器启动参数-XX:+UseG1GC,Java 9开始被作为默认垃圾回收器。
这里基于读者已经了解其他三种GC算法,我作为一个blog搬运工(关于G1算法的优秀blog请看参考资料),谈一下对G1算法的总结和理解思路。

基本概念

GC算法中的并行和并发

并发(concurrent):通常指GC线程与应用程序线程宏观上看是一起执行的

并行(parallel):指多个线程进行垃圾回收,此时应用程序线程是暂停的

三色标记

黑色:该对象与它的子对象都被扫描

灰色:对象本身被扫描,但还没扫描完该对象中的子对象

白色:未被扫描对象,扫描完成所有对象之后,最终为白色的为不可达对象,即垃圾对象

RSet(remembered set)

该集合用来记录并跟踪其它region指向该region中对象的引用,使统计region中存活对象的效率更高

CSet(collection set)

记录了GC要收集的Region集合,集合里的Region可以是任意年代的。在GC的时候,对于old->young和old->old的跨代对象引用,只要扫描对应的CSet中的RSet即可。

SATB

SATB全称snapshot-at-the-beginning,SATB保证了在并发标记过程中新分配对象不会漏标

(1)SATB工作的具体过程

  1. [top, end]区域为空闲区,top向右移来给对象在[next TAMS, top]之间分配内存
  2. 并发标记在[previous TAMS, next TAMS]区间进行,并发标记结束时,将next TAMS所在的地址赋值给previous TAMS,SATB给 [bottom, previous TAMS] 之间的对象创建一个快照Bitmap,所有垃圾对象能通过快照被识别出来
  3. 下一轮开始时,将top所在的地址赋值给next TAMS

(2)SATB write barrier如何解决白色对象被漏标

(引用自[HotSpot VM] 关于incremental update与SATB的一点理解)

一个white对象在并发标记阶段会被漏标的充分必要条件是:

1、mutator插入了一个从black对象到该white对象的新引用

2、mutator删除了所有从grey对象到该white对象的直接或者间接引用。

因此,要避免对象的漏标,只需要打破上述2个条件中的任何一个即可。

Incremental update关注的是第一个条件的打破,即引用关系的插入。Incremental update利用write barrier将所有新插入的引用关系都记录下来,最后以这些引用关系的src为根STW地重新扫描一遍即避免了漏标问题。

SATB关注的是第二个条件的打破,即引用关系的删除。SATB利用pre write barrier将所有即将被删除的引用关系的旧引用记录下来,最后以这些旧引用为根STW地重新扫描一遍即可避免漏标问题。

(3)Write barrier功能

  1. 跨region的引用同步到RSet里
  2. 同步SATB快照的完整性

分代G1算法

G1算法引入了region,分区young gen和old gen没有明确的界限,当新建对象大小超过region大小一半时,直接在新的一个或多个连续region中分配,并标记为Humongous,为巨型对象。

把GC过程分成 global concurrent markingevacuation 两个阶段且相互独立执行的理解更友好。其他的思路我也看过,将GC分为四个阶段,最后一个阶段是筛选回收,这种理解方法并不好,因为真正回收内存的操作并不一定是紧接着在完成所有标记之后。

global concurrent marking 基于SATB

  1. 初始标记(initial marking)暂停阶段
    从GC Roots开始标记直接可达的对象。在G1垃圾回收器中还会存在young GC,这个阶段借用了young GC的暂停

  2. 并发标记(concurrent marking)并发阶段
    从GC Roots开始对堆中的对象标记,标记线程与应用程序线程并行执行,这个过程基于SATB。还会伴随着扫描SATB write barrier记录的引用变化

  3. 最终标记(final marking / remarking)暂停阶段
    处理还未扫描完的SATB write barrier。CMS的remark阶段扫描的是INC write barrier,由于这个无法发现Concurrent Mark期间堆外根集(寄存器、栈)的引用变化,所以CMS的remark阶段可能会很慢。

  4. 筛选回收(cleanup)
    在SATB的Bitmap中统计各个region中存活对象,擦除RSet(Stop the World)。重置空白区域并将其返回到空闲列表(并发执行)

evacuation


暂停阶段
选定需要回收的region加入到CSet中,并行地把CSet中每个region的活对象拷贝到新的region里。

G1 GC选取CSet的模式

  1. Young GC:选定所有young gen里的region。通过控制young gen的region个数来控制young GC的开销。
  2. Mixed GC:选定所有young gen里的region,外加根据global concurrent marking统计得出收集收益高的若干old gen region。在用户指定的开销目标范围内尽可能选择收益高的old gen region。
  3. Full GC:确切地说Full GC算是一种异常情况下的GC,当Mixed GC无法跟上应用程序分配内存的速度时,Java 9及之前会切换到serial old GC/Java 10开始多线程并行,来收集整个GC heap(该GC heap包括Java 7及之前的perm)。
    触发Full GC的情况:global concurrent marking时old gen被填满;对象晋升失败;巨型对象分配失败

G1垃圾回收器

G1 GC的优点

  1. 可预测停顿,维护CSet来尽量使停顿时间小于用户设置的MaxGCPauseMillis。
  2. 基于SATB效率高,remark阶段使用SATB write barrier比CMS的INC write barrier效率高,易于控制停顿
  3. 并行与并发结合
  4. 整合空间,引入region来划分堆,没有了复制算法浪费内存和标记清除法存在大量内存碎片的问题

G1 GC适合在什么场景下使用?

  1. 堆内存较大
  2. Full-GC执行太频繁,或者持续的时间太长
  3. GC暂停时间太长
  4. 对象分配的速度差距较大

G1 GC常用参数解释

具体参数及解释查看垃圾优先型垃圾回收器调优

-XX:MaxGCPauseMillis=200 设置GC的最大暂停时间为200ms。

-XX:G1HeapRegionSize=n 设置的region区域的大小。值是2的n次幂,范围是1MB到32MB之间。

-XX:ParallelGCThreads=n 设置STW工作线程数的值。

-XX:ConcGCThreads=n 设置并行标记的线程数。

-XX:InitiatingHeapOccupancyPercent=45 设置触发标记周期的Java堆占用率阈值。默认占用率是整个Java堆的45%。

-XX:G1ReservePercent=10 设置作为空闲空间的预留内存百分比,以降低目标空间溢出的风险。默认值是 10%

G1 GC调优

参考G1垃圾收集器(六) 之 命令行选项和最佳实践

  1. 不设置young gen大小。通过参数显式设置young gen大小可能会使设定的pause time失效。G1是通过控制young gen的region个数来控制young GC的开销。
  2. MaxGCPauseMillis停顿时间应考虑设置90%以上时间都能达到目标的值。因为MaxGCPauseMillis是最大停顿时间,可以保证大部分停顿时间在设定范围内。
  3. 有关溢出和用尽的日志消息的问题解决。(没有足够的内存,供存活者和/或晋升对象使用。Java堆不能扩展,因为已达到最大值。)

    • 增加 -XX:G1ReservePercent=n 选项的值(并相应增加总的堆大小),为“目标空间”增加预留内存量,默认是10。

    • 减少 -XX:InitiatingHeapOccupancyPercent=45 提前启动标记周期,默认占用率是整个Java堆的 45%。

    • 增加 -XX:ConcGCThreads=n 选项的值来增加并行标记线程的数目。

参考资料: