jvm根节点枚举、安全点、安全区域、记忆集、卡表、写屏障、并发的可达性分析

讲具体的实现之前,先说说几个和这些垃圾回收器息息相关的一些知识点,可以有一个更好的理解

1.根节点枚举

        也就是可达性分析算法从GC Roots集合中找引用链的过程,可作为GC Roots的节点主要在全局性的引用(例如常量或类静态属性)与执行上下文(例如栈帧中的本地变量表)中,虽然目标已经明确,但要逐个检查这里面的引用,那么必然会消耗很多时间。

        迄今为止,所以垃圾回收器在根节点枚举这一步骤都是需要暂停用户线程的,也就是STW,根节点枚举必须在一个保证一致性快照中进行,也就是我在根节点枚举时,你要保证堆中的所有数据的引用不会改变,如果有改变,那么根节点可能会出现误判,这一点很好理解。通过枚举一个一个根节点(GC Roots),然后顺藤摸瓜一路摸下来,然后没摸到的那些对象就把它咔嚓回收了。那这个顺藤摸瓜的过程就必须让世界停止,也就是那些工作线程都得停了。你想想如果不STW那对象引用关系变来变去的,垃圾收集器得怎么咔嚓对象啊,容易咔嚓错了,那咱们使用者不就急眼了啊。所以枚举根节点时STW不可避免,所以只能让STW尽量的短。

        目前主流的jvm都是精准式垃圾回收(也就是它已经知道某个位置上的某个数据的类型,类型是准确的。),这时候只需枚举那些存在地址引用的GCRoot了,进一步减轻了工作量。在HotSpot中是用了一种叫OopMap的结构来存放一个对象内什么偏移量上是什么类型的数据。在类加载过程中就会进行记录。每个方法可能会有好多个OopMap,这是根据特定位置来决定的

2.安全点

        在OopMap的协助下,HotSpot可以快速准确的完成GCRoots枚举,实际上HopSpot每一条指令都生成对应的OopMap,只会在特定的位置记录这些信息,这些位置被称为安全点(Safepoint)。

这些特定的位置主要在:

1、方法临返回前/调用方法的call指令后

2、循环的末尾

3、可能抛出异常的地方

        之所以要在特定的位置才记录OopMap,是因为如果对每条指令都记录一下的话,那就会需要大量的空间,提高了GC的空间成本,所以用一些比较关键的点来记录就能有效的缩小记录所需的空间。

有了安全点的设定,也就规定了用户线程必须达到安全点后才能停止运行,才能进行GC。

3.安全区域

        Safepoint机制保证了程序执行时,在不太长的时间内就会遇到可进入GC的Safepoint。但是,程序“不执行”的时候呢?所谓的程序不执行就是没有分配CPU时间,典型的例子就是线程处于Sleep状态或者Blocked状态,这时候线程无法响应JVM的中断请求,“走”到安全的地方去中断挂起,JVM也显然不太可能等待线程重新被分配CPU时间。对于这种情况,就需要安全区域(Safe Region)来解决。

        安全区域是指在一段代码片段之中,引用关系不会发生变化。在这个区域中的任意地方开始GC都是安全的。我们也可以把Safe Region看做是被扩展了的Safepoint。在线程执行到Safe Region中的代码时,首先标识自己已经进入了Safe Region,那样,当在这段时间里JVM要发起GC时,就不用管标识自己为Safe Region状态的线程了。在线程要离开Safe Region时,它要检查系统是否已经完成了根节点枚举(或者是整个GC过程),如果完成了,那线程就继续执行,否则它就必须等待直到收到可以安全离开Safe Region的信号为止。

4.记忆集

        记忆集是一种用于记录从非收集区域指向收集区域的指针集合的抽象数据结构

前面介绍分代回收的文章中提过记忆集(Remebered Set)的数据结构,给老年代进行分块,记忆集存储着哪一块老年代的区域存在跨带引用。

5.卡表

        记忆集是一种“抽象”的数据结构,抽象的意思是只定义了记忆集的行为意图,并没有定义其行为的具体实现。卡表就是记忆集的一种具体实现,它定义了记忆集的记录精度、与堆内存的映射关系等。 卡表与记忆集的关系,就是HashMap与Map的关系。

        卡表最简单的形式就是一个字节数组,HotSpot就是这样做的

CARD_TABLE [this address >> 9] = 0;

CARD_TABLE中的元素标志的是内存区域中的一块内存,这一个内存被称为卡页。

一个卡页的内存中通常包含不止一个对象,只要卡页内有一个(或更多)对象的字段存在着跨代指针,那就将对应卡表的数组元素的值标识为1,称为这个元素变脏(Dirty),没有则标识为0。在垃圾收集发生时,只要筛选出卡表中变脏的元素,就能轻易得出哪些卡页内存块中包含跨代指针,把它们加入GC Roots中一并扫描。
 

6.写屏障

          卡表元素何时变脏的答案是很明确的——有其他分代区域中对象引用了本区域对象时,其对应的卡表元素就应该变脏,变脏时间点原则上应该发生在引用类型字段赋值的那一刻。但问题是如何变脏,即如何在对象赋值的那一刻去更新维护卡表呢?

        在HotSpot虚拟机里是通过写屏障(Write Barrier)技术维护卡表状态的。写屏障可以看作在虚拟机层面对“引用类型字段赋值”这个动作的AOP切面,在引用对象赋值时会产生一个环形(Around)通知。如下

void oop_field_store(oop* field, oop new_value) {
	// 引用字段赋值操作    
	*field = new_value;    
	// 写后屏障,在这里完成卡表状态更新    
	post_write_barrier(field, new_value); 
}

        除了写屏障的开销外,卡表在高并发场景下还面临着“伪共享”(False Sharing)问题。

伪共享是处理并发底层细节时一种经常需要考虑的问题,现代中央处理器的缓存系统中是以缓存行(Cache Line) 为单位存储的,当多线程修改互相独立的变量时,如果这些变量恰好共享同一个缓存行,就会彼此影响(写回、无效化或者同步)而导致性能降低,这就是伪共享问题。

        假设处理器的缓存行大小为64字节,由于一个卡表元素占1个字节,64个卡表元素将共享同一个缓存行。这64个卡表元素对应的卡页总的内存为32KB(64×512字节),也就是说如果不同线程更新的对象正好处于这32KB的内存区域内,就会导致更新卡表时正好写入同一个缓存行而影响性能。为了避免伪共享问题,一种简单的解决方案是不采用无条件的写屏障,而是先检查卡表标记,只有当该卡表元素未被标记过时才将其标记为变脏,即将卡表更新的逻辑变为以下代码所示

if (CARD_TABLE [this address >> 9] != 0)    
	CARD_TABLE [this address >> 9] = 0;

        在JDK 7之后,HotSpot虚拟机增加了一个新的参数-XX:+UseCondCardMark,用来决定是否开启卡表更新的条件判断。开启会增加一次额外判断的开销,但能够避免伪共享问题,两者各有性能损耗,是否打开要根据应用实际运行情况来进行测试权衡。

7.并发的可达性分析

        前面根节点枚举提过,可达性分析算法必须在一致性的快照中进行分析,意味着必须冻结全部用户的线程。所谓并发,在于用户线程和垃圾回收线程同时运行,如果同时只有一种线程运行,那就不存在并发问题了

        想解决或者降低用户线程的停顿,就要搞清楚为什么必须在保障一致性的快照上才能进行对象图的变量?我们引入三色标记来说明:

  • 白色:表示对象尚未被垃圾收集器访问过,分析结束阶段认为白色表示不可达
  • 黑色:表示对象已经访问过,并且其所有引用的对象也访问过(即该节点被访问过,该节点的所有子节点也被访问过),代表安全存活的对象
  • 灰色:表示该对象被访问过,但是其所有引用的对象都没有被访问过(即该节点被访问过,该节点还有至少一个子节点没有被访问过)

        不过因为是并发可达性分析,在确定了GC Roots之后,我们继续往下扫描,此时用户线程可能会对可达性分析造成干扰,可能出现两种后果:一种是把原本消亡的对象标记为存活,这个对象得等待下次垃圾收集才能被回收;第二种是把原本存活的对象当做垃圾清理了,这种结果就比较致命了,下面将演示这两种错误是如何产生的:

1)初始状态,只有GCRoots是黑色的,只有被黑色对象引用才能存活

 2)扫描过程中,从黑向白进行推进

 3)扫描完成,白色的对象就是可以清除的垃圾

 4)时光回溯,如果用户线程在进行时并发修改了引用关系,就不会这么顺利了。如下灰色的对象正在扫描它的子对象,但用户线程切掉了灰色对象和下面的白色对象的引用,而这条白色对象又同时被前面的黑色对象引用了,黑色对象是不会再重新扫描它的子节点的,所以这个对象就被误判了

造成这只种对象消失的原因在于:

  1. 插入了一条或多条从黑色到白色对象的引用
  2. 删除了全部从灰色对象到白色对象的直接或间接引用

        单独的出现一个条件是无所谓的,比如我们仅仅删除一个旧的引用,此时只是该对象死亡了而已,并没有影响;如果仅仅是添加也是不影响。

所以我们要解决这一问题只需要破坏其中的任意条件即可。由此产生了两种解决方案:增量更新原始快照

增量更新

 破坏第一个条件,当黑色对象插入新的指向白色对象的引用关系时,将新插入的引用记录下来,等并发扫描完后,再将这些记录过的引用关系中的黑色对象为根,重新扫描一次。

原始快照

当灰色对象要删除指向白色对象的引用时,就将这个要删除的引用记录下来,在扫描结束后,再按照原始快照(此时这个引用还未删除)将记录过的引用关系的灰色对象为根,重新扫描一遍

在CMS中,用到了增量更新,而G1中用到了原始快照,下一篇

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值