一文带你透彻理解JVM并发标记中的SATB算法与增量并发标记算法!

本文详细介绍了G1垃圾回收器中的并发标记算法,特别是SATB(Snapshot-At-The-Beginning)策略,以及其在处理老生代分区中的工作原理,包括对象分配、内存管理、标记过程和清理阶段。并发标记旨在减少标记对应用的影响,通过并发工作和位图技术优化内存使用。
摘要由CSDN通过智能技术生成

并发标记

混合回收的前提是老生代完成了标记。为了减少标记老生代对应用的影响,采用了并发标记。

G1中采用的并发标记算法和CMS并发标记算法并不相同。

CMS采用的是增量并发标记,G1中使用的是SATB(Snapshot-At-The-Beginning)算法。

SATB算法介绍

并发标记指的是标记线程和应用程序线程并发运行。那么标记线程如何并发地进行标记?并发标记时,一边标记垃圾对象,一边还在生成垃圾对象,如何正确地标记对象?为了解决这个问题,以前的垃圾回收算法采用串行执行,这里的串行指的是标记工作和对象生成工作不同时进行,而在G1中引入了新的算法。

在介绍并发标记算法之前,我们首先回顾一下对象分配,再来讨论这个问题。

在堆分区中分配对象的时候,对象都是连续分配的,所以可以设计几个指针,分别是bottom、prev、next和top。用bottom指向堆分区的起始地址,用prev指向上一次并发处理后的地址,用next指向并发标记开始之前内存已经使用内存的地址,当并发标记开始之后,如果有新的对象分配,可以移动top指针,使用top指向当前内存分配成功的地址。next指针和top指针之间的地址就是应用程序线程新增对象使用的内存空间。假设prev指针之前的对象已经标记成功,在并发标记的时候从根出发,不仅标记prev和next之间的对象,还标记prev指针之前活跃的对象。当并发标记结束之后,只需要把prev指针设置为next指针即可开始新一轮的标记处理。

prev和next指针解决了并发标记工作内存区域的问题,还需要再引入两个额外的数据结构来记录内存标记的状态,典型的是使用位图(BitMap)来指示哪块内存已经使用而哪块内存还未使用。所以并发标记引入两个位图PrevBitmap和NextBitmap,用PrevBitmap记录prev指针之前内存的标记状态,用NextBitmap记录整个内存从Bottom到next指针之前的标记状态。

很多人都很奇怪,既然NextBitmap包含了整个使用内存的标记状态,为什么要引入PrevBitmap这个数据结构?这个数据结构在什么时候使用?使用PrevBitmap最主要的目的是在本次标记中确定上一次标记过的活跃的对象在本次标记后才可能继续存活,使用该位图可以优化很多内存的管理。下面通过示意图来演示一下并发标记的过程。

假定初始情况如图6-18所示。

图6-18 并发标记初始情况

这里用Bottom表示分区的底部,Top表示分区空间使用的顶部,TAMS指的是Top-At-Mark-Start,prev就是前一次标记的地址,即prev TAMS,next指向的是当前开始标记时最新的地址,即next TAMS。并发标记是从根对象出发开始并发的标记。在第一次标记时PrevBitmap为空,NextBitmap待标记。

开始进行并发标记,结束后如图6-19所示。

图6-19 并发标记结束后的状态

并发标记结束后,NextBitmap记录了分区对象存活的情况,假定上述的位图中黑色区域表示堆分区中对应的对象还活着。在并发标记的同时,应用程序继续运行,所以Top指针发生了变化,继续增长。

这个时候,可以认为NextBitmap中活跃对象及Next TAMS和Top之间的对象都是活跃的。在进行垃圾回收的时候,如果分区需要被回收,则会复制这些对象;如果分区可用空间比较多,则不需要回收分区。当应用程序继续执行、新一轮的并发标记启动时,初始状态如图6-20所示。

图6-20 新一轮并发标记启动时的状态

在新一轮的并发标记开始时,交换BitMap,重置指针。根据根对象对Bottom和Next TAMS之间的内存对象进行标记,标记结束后的状态如图6-21所示。

图6-21 第二次并发标记结束后状态

当标记完成时,如果分区垃圾对象满足一定条件(如分区的垃圾对象占用的内存空间达到一定的数值),分区就可以被回收。

SATB算法的核心是建立一个内存切片,当应用程序和并发标记工作线程对同一个对象进行修改时,使用写屏障记录对象引用关系修改前的对象,然后在合适的时机再对修改对象进行标记。注意,SATB产生的浮动垃圾通常比IU更多。

增量并发标记算法

老生代分区的回收依赖于G1的并发标记算法,这个过程称为“并发标记阶段”。并发标记是指并发标记线程和应用程序线程同时运行,它有4个典型的子阶段:初始标记子阶段、并发标记子阶段、再标记子阶段和清理子阶段。在执行并发标记之前,还需要一个并行的根处理阶段,用于识别并发标记的根对象。具体介绍如下。

1. 根处理子阶段

此阶段负责标记所有从根集合直接可达的对象。根集合是对象图的起点,初始标记需要将应用程序线程暂停,即需要一个STW的时间段。在并发标记中的初始标记子阶段和新生代的初始标记几乎一样。实际上并发标记的初始标记子阶段是借用了新生代回收的结果,即以新生代垃圾回收后的新生代Survivor分区作为根,所以并发标记一定发生在新生代回收之后,不需要再进行一次初始标记,这就是所谓的“借道”。

并发标记是以Survivor分区为根对整个老生代进行标记。那么这样做有没有问题?

实际上存在Java根直接引用到老生代对象,且没有任何新生代对象到老生代对象的引用的情况,因此仅以Survivor分区为根对整个老生代进行标记,并不是对老生代的完全标记,因为老生代分区里面可能存在一些活跃对象是通过Java根到达的。

这些对象在并发标记的时候并不会被标记,导致可能存活的对象因没有标记而被错误地回收。从这一点来说,仅以Survivor分区为根开始标记是不够的,需要把那些直接从根出发引用到老生代或者大对象分区的引用补上。这就是说,简单“借道”是不够的,需要针对触发并发标记的Minor GC中增加额外的逻辑。

2. 并发标记子阶段

当Minor GC执行结束之后,如果发现满足并发标记的条件,并发线程就开始进行并发标记,根据新生代的Survivor分区开始并发标记。并发标记的时机是在Minor GC后,只有内存消耗达到一定的阈值才会触发。在G1中,这个阈值通过参数
InitiatingHeapOccupancy-Percent(默认值是45,表示当前已经分配的内存加上本次待分配的内存超过内存总容量的45%就可以启动并发标记)控制。多个并发标记线程同时执行,每个线程每次只扫描一个分区,从而标记出存活对象。在标记的时候还会计算存活对象的数量及存活对象所占用的内存大小,并计入分区空间。

并发标记子阶段会对所有老生代分区的对象进行标记。这个阶段并不需要STW,标记线程和应用程序线程并发运行,使用SATB算法进行并发标记。

3. 再标记子阶段

再标记是最后一个标记阶段。在该阶段中,G1需要一个STW的时间段,找出所有未被访问的存活对象,同时完成存活内存数据计算。引入该阶段的目的是能够达到结束标记的目标。要结束标记的过程,需要满足以下3个条件:

1)在从根(Survivor)出发的并发标记子阶段已经标记出所有的存活对象。

2)标记栈是空的。

3)所有的引用变更对象都被处理了。这里的引用变更对象包括新增空间分配的对象和引用变更对象,新增空间中的所有对象都被认为是活跃的(即便是对象已经死亡也没有关系,在这种情况下只会增加一些浮动垃圾),引用变更处理的对象通过一个队列记录,在该子阶段会处理这个队列中的所有对象。

前两个条件是很容易达到的,但是最后一个条件是很难达到的。如果不引入一个STW的再标记过程,那么应用会不断地更新引用,也就是说,会不断地产生新的引用变更,因而永远也无法达成完成标记的条件。

4. 清理子阶段

再标记子阶段之后是清理子阶段,该子阶段也需要一个STW的时间段。清理子阶段主要执行以下操作:

1)统计存活对象,统计的结果将会用来排序分区,用于下一次的垃圾回收时分区的选择。

2)交换标记位图,为下次并发标记做准备。

3)把空闲分区放到空闲分区列表中,这里的空闲分区指的是全都是垃圾对象的分区。如果分区中还有任何活跃对象都不会释放,真正释放的动作是在混合回收中。

该阶段比较容易引起误解的地方在于,清理子阶段并不会清理垃圾对象,也不会执行存活对象的复制。也就是说,在极端情况下,该阶段结束之后,空闲分区列表将毫无变化,JVM的内存使用情况也毫无变化。

在并发标记阶段完成之后,在下一次进行垃圾回收的时候就会回收垃圾比较多的老生代分区,这时进行的垃圾回收称为混合回收。整个G1垃圾回收的活动图如图6-22所示。

图6-22 G1垃圾回收活动图

  • 20
    点赞
  • 15
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值