g1垃圾收集器

gc

Posted by CHuiL on August 21, 2021

G1收集器(Garbage-First Garbage Collector)

G1是服务器类型的垃圾收集器,适合多处理器大内存的机器运行。他的目标是兼顾高吞吐量和响应时间,满足用户定义的暂停时间目标。在对整个堆进行例如标记操作的过程中,应用的线程也是同时在执行的。

在G1执行一个并发的全局标记之后,就确定了堆中对象的生存情况,也知道了哪些region里面大部分都是要回收的对象,g1会首先回收这些region,而这些通常会产生大量的空闲内存。这就是为什么g1叫做 Garbage-First,意思就是优先收集垃圾对象。g1使用暂停预测模型来满足用户定义的stw目标时间,他根据指定的stw时间目标来选择要收集的region数量来。

g1可以满足用户设定的暂停时间目标,但是不是绝对的,而是高概率满足,他是根据前面的数据,以及一个相当精确的region收集模型来确定暂停时间目标内可以收集哪些region和多少region。

必要概念理解

region

G1中将堆划分为一个个大小相同的内存区域,称为region。每个region都是一片连续的虚拟内存。

region的划分,在逻辑上也是分代的,分为young和old,young中又分为eden 和 两个survivor。对象的分配都会在这些年轻的region上完成,一旦年轻代满了,就会对这些年轻代region进行垃圾回收。

Collection Set

该集合表示需要被回收的region集合。该集合会根据目标停顿时间进行选择和调整。这也是g1实现控制stw时间的主要工具。

young gc 和mix gc

g1的gc跟以往的垃圾收集器不同,他的gc过程基本都是标记-复制算法,没有youung和old gc的区分。只有young gc和mix gc。g1的gc就是对Collection Set中的region进行回收。而且young gc时, CS中只有young region,mix gc时 CS中有 young region 和部分 old region。

image

g1会将存活的对象从一个或多个region复制到另外一个空的region上,并在这个过程中压缩和释放内存,根据对象的存活年龄,老对象会晋升到老年代region。这个操作是在多处理器下并行执行的,以便减少暂停时间,提高吞吐量。因为,每一次的gc,g1都在不断地减少碎片。
对比cms,cms只是收集垃圾不进行整理,而Parallel GC是对整个堆进行压缩,这会导致很长暂停时间

除此之外,对于大小大于region一半的大对象,则直接当做是old region。如上图中的H就是大对象。当一个对象的大小超过region大小的一半时,即被定义为大对象。

大对象

  • 大小大于等于region一半的对象
  • 直接分配到了old gen,防止了反复拷贝移动
  • 在global concurrent marking阶段的clean up和full gc阶段回收
  • 在分配H-obj之前先检查是否超过 initiating heap occupancy percent和the marking threshold, 如果超过的话,就启动global concurrent marking,为的是提早回收,防止 evacuation failures 和 full GC。

分配(疏散)失败

分配失败的问题,CMS和G1都是并发的垃圾收集器,在收集的过程中应用程序也在不断运行,那就存在回收的速度跟不上分配的速度的风险,而这将导致垃圾回收失败。在g1进行存活对象复制时,如果找不到可用的空闲region来将存活对象复制过去,这就产生分配失败

暂停

g1会在将存活对象移动到新区域时暂停应用线程。这个暂停可以是只针对年轻代region的垃圾收集,也可以是针对年轻代和老年代region的混合型垃圾收集。与CMS一样,g1会有一个用来进行最终标记的暂停。不同于cms的是,cms会有一个初始化的暂停,而g1将初始化标记作为疏散暂停的一部分。g1在收集垃圾的最后会有一个清理阶段,他会有一部分是STW的,一部分是concurrent的。而STW期间,g1会标识空的region,并确定作为下一次收集的候选旧region。

Pause Prediction Model

Pause Prediction Model 即停顿预测模型。G1根据这个模型统计计算出来的历史数据来预测本次收集需要选择的Region数量,从而尽量满足用户设定的目标停顿时间。

SATB

Snapshot-At-The-Beginning

为了保证并发标记的正确性,需要使用额外的技术来保证其安全。而这里的并发问题其实可以理解为

  • 一个活得对象被错误认为是死
  • 一个死的对象被认为是活得。

对于第一种情况,在三色标记法算法的情况下,一般会出现在

  • new的新对象被黑色对象引用
  • 一个还没被扫描到的白色对象被重新赋值给了黑色对象,同时没有其他灰色对象对其进行引用

SATB的意思其实就是要保证gc开始时,逻辑上保存了对当前所有对象的存活情况快照。其实在这个模型下,只要依靠两个top-at-mark-start(TAMS)指针,分别为prevTAMS和nextTAMS. image

假设第n轮并发标记开始,将该Region当前的top指针赋值给next TAMS,在并发标记标记期间,分配的对象都在[next TAMS, top]之间,所以我们知道哪些对象是新分配的,有了这个隐式的标记,我们就能对新分配的对象进行扫描(置灰)。

而在之前的对象,如果一个应用的对象被修改替换了,会通过write barrier将旧引用记录下来(置灰)。而这是SATB要保证的,在标记开始时存活的对象也都认识是存活的。

而这就会导致前面提到的第二个问题,一个已经不被引用的对象会被错误的认为是活的,这种称为浮动垃圾,不过这种问题不大,在下一次gc就会被回收掉。

Remembered Set

RSet记录了其他Region中的对象引用本Region中对象的关系,属于points-into结构(谁引用了我的对象);

首先明白,Rs要解决的是什么问题。我们知道在young gc时,只收集young region中的对象。在我们在从GC root开始扫描对象时,可能过会漏掉那些被old region引用的对象,如下图。

image

BC都为old region,A为young region。如果从GC Root出发,图中?号的两个对象对GC Root来说是不可达的。为了解决这个问题,在没有RS的情况下,可能就需要把所有的old region作为root来进行扫描,而这就会导致young gc花费更多的时间,所以利用Rs来记录下old->young的引用关系,这就除了扫描GC root,在额外扫描所有young collection的Rs,找到有引用cs的old region为root,可以大大减少GC的工作量。

而在mix gc的时候,有old->old的关系,而young->old 的关系由扫描全部young region 得到,所以也可以避免扫描所有old region。

image

G1过程

在介绍之前,我们需要先知道,g1整个运行的过程中,有几个大的阶段

  • young gc:整体是STW的,只负责回收young region的垃圾
  • 标记过程:整个过程有多个阶段,其中一些是stw,一些是concurrent
  • mix gc:整体和young gc差不多,不过这里回收的是young 和部分old region;混合垃圾收集通常是由多个混合垃圾收集周期组成。当收集了足够数量的老年代区域时,G1 收集器会再次执行年轻代垃圾收集,直至下一次标记周期完成。

g1以上的各个过程并不是说单独运行的,而是混杂着进行的。比如在并发标记的过程中,就可能会启动youung gc。而启动并发标记也需要先启动一次young gc来完成初始化标记阶段。而在并发标记结束之后,根据标记的情况在决定是否启动mix gc。

image 如上图,每一个小蓝点表示一次young gc。在达到设定的阈值后(–XX:InitiatingHeapOccupancyPercent 表示使用堆空间占总空间的比率,默认45),会启动并发标记

并发标记开始是会由一个stw的young gc开始,并在young gc中完成中的Initial Mark Phase(通过了解young gc就明白这一步其实young gc可以完成)在完成young gc之后,就开始并发标记的后续阶段。而并发标记中的标记阶段是并发的,而在并发的过程中,应用线程也在执行,同时,young gc也可以执行,当然,在执行young gc的过程中,并发标记是会被停止的,毕竟是stw。

而在并发标记结束之后,会知道老年代中的存活情况,并在达到一定条件后(怎么决定是否启动老年代,如何知道老年代region?),启动一次mix gc。mix gc过程和young gc基本一致,只是回收的对象对了old region。并且他是由多个mix gc组成,当G1认为疏散更多老年代region不能产生足够多的空闲空间时,就会结束该过程。

young gc各个阶段

0.189: [GC pause (young), 0.00080776 secs]
   [Parallel Time: 0.4 ms]
      [GC Worker Start (ms): 188.7 188.7 188.8 188.8
       Avg: 188.8, Min: 188.7, Max: 188.8, Diff: 0.1]
      [Ext Root Scanning (ms): 0.2 0.2 0.2 0.1
       Avg: 0.2, Min: 0.1, Max: 0.2, Diff: 0.1]
      [Update RS (ms): 0.0 0.0 0.0 0.0
       Avg: 0.0, Min: 0.0, Max: 0.0, Diff: 0.0]
         [Processed Buffers : 0 0 0 1
          Sum: 1, Avg: 0, Min: 0, Max: 1, Diff: 1]
      [Scan RS (ms): 0.0 0.0 0.0 0.0
       Avg: 0.0, Min: 0.0, Max: 0.0, Diff: 0.0]
      [Object Copy (ms): 0.2 0.2 0.1 0.2
       Avg: 0.2, Min: 0.1, Max: 0.2, Diff: 0.0]
      [Termination (ms): 0.0 0.0 0.0 0.0
       Avg: 0.0, Min: 0.0, Max: 0.0, Diff: 0.0]
         [Termination Attempts : 1 2 1 2
          Sum: 6, Avg: 1, Min: 1, Max: 2, Diff: 1]
      [GC Worker End (ms): 189.1 189.1 189.1 189.1
       Avg: 189.1, Min: 189.1, Max: 189.1, Diff: 0.0]
      [GC Worker (ms): 0.4 0.4 0.3 0.3
       Avg: 0.4, Min: 0.3, Max: 0.4, Diff: 0.1]
      [GC Worker Other (ms): 0.0 0.0 0.1 0.1
       Avg: 0.1, Min: 0.0, Max: 0.1, Diff: 0.1]
   [Clear CT: 0.2 ms]
   [Other: 0.2 ms]
      [Choose CSet: 0.0 ms]
      [Ref Proc: 0.2 ms]
      [Ref Enq: 0.0 ms]
      [Free CSet: 0.0 ms]

在young gc开始前,会在遵循用户设置的GC暂停时间上限的基础上,选择一个最大年轻带区域数,将这个数量的所有年轻代区域作为收集集合。

1. External Root Scanning

扫描外部的根引用,如寄存器,线程栈等指向指向收集集合(CS),这部分是多个线程并行执行的。

从GC root出发,扫描被GC root直接引用的对象,并直接复制到s region。引用关系上的其他对象放入标记栈中。

2.Update Remembered Sets (RSets)

更新Rset。由于对象间引用关系的变更,不是直接就更新的Rs。而是会用log的信息记录下来。这里需要将dirty数据进行更新,以准确反映对象中的引用关系。

3.Scan RSets

扫描Rs。以Rs中的old region作为根,扫描存活的对象,同样直接引用的直接复制到s region中,引用链路上的其他对象放入标记栈中。

4.Object Copy

遍历标记栈,将栈内的所有对象移动至Survivor区域,而复制年龄达到阈值的会晋升到old region

5.收尾的工作

  • Clear CT(Card Table):清除Rset的Card table
  • Choose Collection Set(CS):每一次gc都会更新CS。
  • Free CSet:(清理回收集合)
  • Reference Processing:处理先前垃圾收集阶段推迟处理的引用

image

并发标记

0.078: [GC pause (young) (initial-mark), 0.00262460 secs]
   [Parallel Time: 2.3 ms]
      [GC Worker Start (ms): 78.1 78.2 78.2 78.2
       Avg: 78.2, Min: 78.1, Max: 78.2, Diff: 0.1]
      [Ext Root Scanning (ms): 0.2 0.1 0.2 0.1
       Avg: 0.2, Min: 0.1, Max: 0.2, Diff: 0.1]
      [Update RS (ms): 0.2 0.2 0.2 0.2
       Avg: 0.2, Min: 0.2, Max: 0.2, Diff: 0.0]
         [Processed Buffers : 2 3 2 2
          Sum: 9, Avg: 2, Min: 2, Max: 3, Diff: 1]
      [Scan RS (ms): 0.0 0.0 0.0 0.0
       Avg: 0.0, Min: 0.0, Max: 0.0, Diff: 0.0]
      [Object Copy (ms): 1.8 1.8 1.8 1.8
       Avg: 1.8, Min: 1.8, Max: 1.8, Diff: 0.0]
      [Termination (ms): 0.0 0.0 0.0 0.0
       Avg: 0.0, Min: 0.0, Max: 0.0, Diff: 0.0]
         [Termination Attempts : 1 1 1 1
          Sum: 4, Avg: 1, Min: 1, Max: 1, Diff: 0]
      [GC Worker End (ms): 80.4 80.4 80.4 80.4
       Avg: 80.4, Min: 80.4, Max: 80.4, Diff: 0.0]
      [GC Worker (ms): 2.2 2.2 2.2 2.2
       Avg: 2.2, Min: 2.2, Max: 2.2, Diff: 0.1]
      [GC Worker Other (ms): 0.0 0.1 0.1 0.1
       Avg: 0.1, Min: 0.0, Max: 0.1, Diff: 0.1]
   [Clear CT: 0.2 ms]
   [Other: 0.2 ms]
      [Choose CSet: 0.0 ms]
      [Ref Proc: 0.1 ms]
      [Ref Enq: 0.0 ms]
      [Free CSet: 0.0 ms]
   [Eden: 3072K(5120K)->0B(5120K) Survivors: 1024K->1024K Heap: 16M(32M)->16M(32M)]
  [Times: user=0.06 sys=0.00, real=0.00 secs] 
   0.081: [GC concurrent-root-region-scan-start]
   0.082: [GC concurrent-root-region-scan-end, 0.0009122]
   0.082: [GC concurrent-mark-start]
<snip> [Zero or more embedded young garbage collections are possible here,
          but removed for brevity.]
   0.094: [GC concurrent-mark-end, 0.0115579 sec]
   0.094: [GC remark 0.094: [GC ref-proc, 0.0000033 secs], 0.0004374 secs]
          [Times: user=0.00 sys=0.00, real=0.00 secs] 
   0.094: [GC cleanup 22M->10M(32M), 0.0003031 secs]
          [Times: user=0.00 sys=0.00, real=0.00 secs] 
   0.095: [GC concurrent-cleanup-start]
   0.095: [GC concurrent-cleanup-end, 0.0000350]

1. Initial Mark

该阶段会STW。并且该阶段是伴随着一次young gc一起完成的。这一阶段的目的是标记由Gc Root直接引用的对象。将这些对象标记出来,作为下一阶段的root。

2.Root Region Scanning

从surivivor region的root出发,扫描标记被引用的老年代中的对象。该扫描运行期间,不需要stw,不过需要在young gc之前执行完。

3.Concurrent Marking

找出整个堆中存活的对象。该阶段是与引用线程并发执行的。并且该阶段可能会被young gc暂停。

4.Remark

在该阶段完成标记,该阶段会STW。在这段时间,g1会将每个java线程中的STAB write barrier记录引用处理完,同时还会处理引用。 如果在前面的阶段发现了一些已经为空的region,则会在这个阶段被删除和重置。

5. Cleanup

该阶段有STW,也有并行的。

  • 统计存活对象和完成region的释放 STW
  • 清除RS STW
  • 重置region,返回空的region到空闲region列表中。 并发的

6. Copying

该阶段其实已经不算是并发标记中的一部分了,已经标记已经完成。标记完成之后,后续就可能进行垃圾回收,这里交由young gc来做,也可能是mix gc。

mix gc

在完成并发标记之后,就知道了old region中的信息。此时就根据参数选项设置,来决定是否执行mix gc,知道满足条件后,就又会照常执行young gc了。

注意,young gc和mix gc的过程是一样的。不同的在于CS,对young gc来说,CS包含的是选定后需要被回收的young region。而mix gc中的cs包含所有的young regino,和“活性”最低的那些old region,依次来满足对垃圾占用,暂停时间要求的目标。

相关参数

  • -XX:G1HeapRegionSize:设置Region大小 1M~32N,如果不指定,G1会根据Heap大小自动决定
  • XX:InitiatingHeapOccupancyPercent设置的值后,会开始并发的标记。这个值默认是45;
  • MaxGCPauseMillis:暂停时间目标,默认是200ms

mix gc相关的参数

  • –XX:G1MixedGCLiveThresholdPercent:老年代区域中的存活对象的占用率阈值,这些区域会包含在mix gc中
  • –XX:G1HeapWastePercent:你能够忍受的堆内存中的垃圾占用率阈值。
  • –XX:G1MixedGCCountTarget:对老年代区域执行混合垃圾手机的目标次数,这些区域的存活数据的占用率不会超过G1MixedGCLiveThresholdPercent 选项设定的值。
  • –XX:G1OldCSetRegionThresholdPercent:在一次混合垃圾收集期间,可以收集的老年代区域的最大数量的限制。

  • -XX:+UseG1GC 使用G1 GC。
  • -XX:MaxGCPauseMillis=n 设置最大GC停顿时间,这是一个软目标,JVM会尽最大努力去达到它。
  • -XX:InitiatingHeapOccupancyPercent=n 启动并发标记循环的堆占用率的百分比,当整个堆的占用达到比例时,启动一个全局并发标记循环,0代表并发标记一直运行。默认值是45%。
  • -XX:NewRatio=n 新生代和老年代大小的比例,默认是2。
  • -XX:SurvivorRatio=n eden和survivor区域空间大小的比例,默认是8。
  • -XX:MaxTenuringThreshold=n 晋升的阈值,默认是15(一个存活对象经历多少次GC周期之后晋升到老年代)。
  • -XX:ParallelGCThreads=n 设置GC并发阶段的线程数,默认值与JVM运行平台相关。
  • -XX:ConcGCThreads=n 设置并发标记的线程数,默认值与JVM运行平台相关。
  • -XX:G1ReservePercent=n 设置保留java堆大小比例,用于防止晋升失败/Evacuation Failure,默认值是10%。
  • -XX:G1HeapRegionSize=n 设置Region的大小,默认是根据堆的大小动态决定,大小范围是[1M,32M]

参考