JVM垃圾回收器详解:OpenJ9中的实时垃圾回收器Metronome介绍

扩展阅读:OpenJ9中的实时垃圾回收器Metronome介绍

OpenJ9中提供了一个实时垃圾回收器Metronome,默认的停顿时间为3毫秒,吞吐率达到70%,停顿时间和期望吞吐率可以通过参数设置。Metronome是一个软实时的垃圾回收器,在大多数情况下可以保证停顿时间,但是可能仍然存在超时的情况。当然要实现实时垃圾回收,不能一次性回收太多内存,所以Metronome也采用分区的设计,其分区的设计思路及大对象的处理与Balanced GC相同。

从使用角度来看,Metronome达到的效果类似于并发回收器的效果:满足大内存的使用,停顿时间在毫秒级。但是JVM中提供的实现方法都是将回收算法通过并发化实现的。而Metronome是一个非常典型的增量标记清除回收,其本质是将并行垃圾回收阶段划分为更小的阶段,在每一个小阶段完成后,检测是否需要放弃执行垃圾回收,当停顿时间达到后就会放弃垃圾回收的执行,从而尽量保证停顿时间满足用户的期望。标记清除和Metronome的对应关系如图7-21所示。

图7-21 标记清除和Metronome的对应关系

Metronome的设计思路并不复杂,就是将大任务拆解成小任务,然后在小任务结束后判断是否需要放弃垃圾回收,如果需要则终止垃圾回收返回应用执行,如果不需要则继续执行。当然,由于Metronome也可能存在回收速率赶不上内存的分配速率的问题,会导致应用内存分配请求失败。此时不再中断小任务的执行,会一直运行,直到本次垃圾回收执行完毕(相当于降级回收)。

但这种实现也有其独特的地方,这里介绍Metronome中Thread栈标记任务拆解:对于根在遍历时会并行地一个一个处理,当处理到某一个线程栈时发现达到了最大执行时间,就会放弃执行垃圾回收;当再次进入垃圾回收过程时会继续执行线程栈的标记任务。 这就是典型的线程栈增量并发标记,如图7-22所示。

图7-22 线程栈增量并发标记

增量并发栈扫描的难度是,如果已经扫描的栈在并发运行后,栈中指针指向了未扫描的线程栈的指针,将导致正确性问题。

例如,T1线程栈完成了扫描,此时放弃根标记,但T2、T3线程栈尚未完成扫描。在继续执行应用时,如果将T1中栈引用修改到T2中栈引用到的对象,因为T1线程栈已经完成了标记,但是T2的线程栈尚未标记,所以必须对T1线程栈修改的对象进行再标记,否则将导致漏标记。

这里仍然可以使用三色标记法来分析正确性。把已经完成标记的线程栈表示为黑色的,扫描完成的线程栈中的指针指向的对象是灰色的,尚未扫描的栈及栈中指针指向的对象都是白色的。要保证标记的正确性,只要不让黑色对象存在指向白色对象的指针即可。所以需要设计屏障,保证已经扫描的栈不能指向未扫描栈的引用。

SATB是一种典型的屏障技术,G1、Shenandoah、Balanced GC等都使用过。使用SATB主要是针对删除对象进行再标记,同时认为新分配的对象都是活跃对象。Metronome也用SATB屏障技术保证标记的正确性(与SATB屏障相关的知识请参考6.4.1节),然而仅仅使用SATB屏障还不能满足Metronome的增量线程栈,Metronome对于增量线程栈的标记采用的是Double屏障,Double屏障指的是不仅记录SATB屏障删除对象,还会记录新指向的对象,然后对所有记录的对象进行再标记。我们先来介绍一下Double屏障,然后再介绍为什么需要Double屏障。

假定有一个线程T1,其中有一些局部变量,如T1a、T1b指向堆空间中的对象。假设初始状态如图7-23所示。

图7-23 应用运行初始状态

假设应用执行修改对象引用关系,例如执行O1.f2 = O2这样的代码,Double屏障需要继续标记O3和O2两个对象(其中O3是SATB屏障记录,O2是新增屏障记录),然后对这两个对象进行再标记,如图7-24所示。

图7-24 Double屏障示意图

为什么需要Double屏障?SATB在哪种情况下可能导致问题?下面继续完善这个例子来演示Double屏障的必要性。假设有3个线程,线程栈分别存在一些变量访问堆中的对象,如图7-25所示。

图7-25 3个线程运行时某一时刻的状态

假设只使用SATB屏障,看看是否满足标记的正确性。假设线程T2和T3都可以通过局部变量指向对象O1,其中T2执行T2a.f2 = T2b(等价于O1.f2 =O2),T3执行T3a.f2 = T3b(等价于O1.f2 = O4)。那么SATB屏障会记录哪些对象(注意,SATB屏障记录引用关系修改前的对象)?

答案是不确定。最主要的原因是T2和T3并发执行,SATB屏障记录的对象与它们执行时看到的引用关系相关。

1)假设T2先执行,T3后执行。T2执行时看到O1.f2=O3,SATB会记录对象O3;T3执行时看到O1.f2=O2,SATB会记录对象O2,所以SATB会记录O3和O2。

2)同样,如果T3先执行,T2后执行,那么SATB会记录O2和O3。

3)如果T3和T2同时执行,即T2和T3看到的都是O1.f2=O3,当T2和T3执行时都只会记录O3。

当T2和T3同时执行时就会产生问题,我们来构造这样的一个场景:i.垃圾回收中完成T1的线程栈扫描,可以认为T1的线程栈是黑色的,此时放弃垃圾回收继续执行。

ii.线程T2执行代码T2a.f2 = T2b,线程T3执行代码T3a.f2 = T3b,且线程T2和线程T3同时执行。

iii.线程T1执行代码T1b = T1a.f2。

iv.线程T2执行代码T2b = null。

v.垃圾回收恢复执行,增量执行线程栈的标记,对线程T2进行标记。

假设只使用SATB屏障,可以看到仅仅O3被记录,但是线程T1中有一个指针T1b指向O2,但是O2是一个未被扫描的对象。T1的线程栈已经完成扫描(黑色节点),但是指向了一个未被扫描的对象(白色节点),这就打破了黑色节点不能指向白色节点的要求。出现这种情况就意味着发生了漏标(即O2漏标)。上述执行过程的最终状态如图7-26所示。

图7-26 使用SATB屏障导致漏标

而使用了Double屏障,将修改前后的对象都设置为灰色,则可以避免问题。但是Double 屏障的成本并不低,所以只需要在增量标记线程栈时使用Double屏障,当所有的线程栈标记完成后,只需要SATB屏障即能保证正确性。

另外,在Metronome中尽量避免锁的使用,当使用锁时需要进入操作系统的内核态,需要比较长的时间,这将影响垃圾回收返回应用的时间。所以Metronome尽量使用无锁的数据结构,例如在并行/并发标记阶段,除了使用本地的标记栈外,当多个线程操作一个公共的标记栈时(如根标记结束后,所有GC工作线程进行标记工作),就需要锁保证多个线程的正确性,为此Metronome设计了无锁的栈结构。

  • 11
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值