【深入理解Java虚拟机】第三章 垃圾收集器与内存分配策略(三)

3.4 HotSpot的算法细节实现

(4.1) 根节点枚举

根节点枚举必须在一个能保障一致性的快照中才得以进行——这里“一致性”的意思是整个枚举期间执行子系统看起来就像被冻结在某个时间点上, 不会出现分析过程中, 根节点集合的对象引用关系还在不断变化的情况。

hotspot是采用一组OopMap对象实现准确式垃圾收集的,一旦类加载动作完成的时候,HotSpot就会把对象内什么偏移量上是什么类型的数据计算出来进行记录,而且在即时编译 过程中, 也会在特定的位置记录下栈里和寄存器里哪些位置是引用。 这样收集器在扫描时就可以直接得知这些信息了, 并不需要真正一个不漏地从方法区等GC Roots开始查找。

(4.2) 安全点

在加载完成时可以记录GC Root信息,但是运行中也会有很多导致OopMap变化的指令,这些如何处理呢。

如4.1所说及时编译时会在特定的位置记录下栈里和寄存器里哪些位置是引用,这个特定位置即为安全点,那么如何定义特定位置效率最高呢?

安全点位置的选取基本上是以“是否具有让程序长时间执行的特征”为标准 进行选定的,因为每条指令执行的时间都非常短暂,程序不太可能因为指令流长度太长这样的原因而 长时间执行,“长时间执行”的最明显特征就是指令序列的复用,例如方法调用、循环跳转、异常跳转 等都属于指令序列复用,所以只有具有这些功能的指令才会产生安全点。

如何在垃圾收集发生时让所有线程(这里其实不包括 执行JNI调用的线程)都跑到最近的安全点,然后停顿下来呢?

(1). 抢先式中断(目前几乎没有虚拟机使用)

在垃圾收集发生时,系统首先把所有用户线程全部中断,如果发现有用户线程中断的地 方不在安全点上,就恢复这条线程执行,让它一会再重新中断,直到跑到安全点上。

(2).主动式中断

在垃圾收集需要中断线程的时候,不直接对线程操作,仅仅简单地设置一 个标志位,各个线程执行过程时会不停地主动去轮询这个标志,一旦发现中断标志为真时就自己在最 近的安全点上主动中断挂起。

ps: 。HotSpot使用内存保护陷阱的方式, 把轮询安全点中断操作精简至只有一条汇编指令的程度。感兴趣的可以深入了解一下。

(4.3) 安全区域

如上描述,安全点解决了运行时程序OopMap收集的问题,那么一些程序“不执行”的時候(例如线程处于Sleep状态或者Blocked状态)就需要引入安全区域来解决此中情况的一致性问题了。

当用户线程执行到安全区域里面的代码时,首先会标识自己已经进入了安全区域,那样当这段时 间里虚拟机要发起垃圾收集时就不必去管这些已声明自己在安全区域内的线程了。当线程要离开安全 区域时,它要检查虚拟机是否已经完成了根节点枚举(或者垃圾收集过程中其他需要暂停用户线程的 阶段),如果完成了,那线程就当作没事发生过,继续执行;否则它就必须一直等待,直到收到可以 离开安全区域的信号为止。我们也可以把安全区域看作被扩展拉伸了的安全点。

(4.4) 记忆集与卡表

记忆集是一种用于记录从非收集区域指向收集区域的指针集合的抽象数据结构。用于解决跨代引用的垃圾收集问题。

如果我们不考虑效率和成本问题,我们可以用一个数组存储所有有指针指向新生代的老年代对象。但是如果这样的话我们维护成本就很好,打个比方,假如所有的老年代对象都有指针指向了新生代,那么我们需要维护整个老年代大小的记忆集,毫无疑问这种方法是不可取的。因此我们引入了卡表的数据结构。下面列举了一些可供选择(当然也可以选择这个范 围以外的)的记录精度

  • 字长精度:每个记录精确到一个机器字长(就是处理器的寻址位数,如常见的32位或64位,这个 精度决定了机器访问物理内存地址的指针长度),该字包含跨代指针。
  • 对象精度:每个记录精确到一个对象,该对象里有字段含有跨代指针。
  • 卡精度:每个记录精确到一块内存区域,该区域内有对象含有跨代指针。

我们以卡精度来进行举例:

卡表的每个元素(1byte)都对应着一块固定大小的数据区域(卡页Card Page,如图大小为512bytes),当老年代中的某个卡页内出现了对象持有了新生代对象的引用时,JVM就把这个对象对应的Region标记为dirty(bit位设置为1),这样在Minor GC时就不用扫描整个老年代,而是扫描Region为Dirty对应的那些内存区域。
在这里插入图片描述

(4.5) 写屏障

我们已经解决了如何使用记忆集来缩减GC Roots扫描范围的问题,但还没有解决卡表元素如何维 护的问题,例如它们何时变脏、谁来更改对应的“变脏”状态等。

★ 何时变脏:有其他分代区域中对象引用了本区域对象时,其对应的 卡表元素就应该变脏,变脏时间点原则上应该发生在引用类型字段赋值的那一刻。

★ 如何更新变脏状态:HotSpot通过写屏障在对应卡页出现跨代引用赋值的时候进行脏标记。

写屏障可以理解为赋值的AOP操作,简化逻辑如下:

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

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

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

(4.6) 并发的可达性分析

书中以三色标记法来描述遍历对象的行为:

  • 白色:表示对象尚未被垃圾收集器访问过。显然在可达性分析刚刚开始的阶段,所有的对象都是 白色的,若在分析结束的阶段,仍然是白色的对象,即代表不可达。
  • 黑色:表示对象已经被垃圾收集器访问过,且这个对象的所有引用都已经扫描过。黑色的对象代 表已经扫描过,它是安全存活的,如果有其他对象引用指向了黑色对象,无须重新扫描一遍。黑色对 象不可能直接(不经过灰色对象)指向某个白色对象。
  • 灰色:表示对象已经被垃圾收集器访问过,但这个对象上至少存在一个引用还没有被扫描过。

在这里插入图片描述

当且仅当以下两个条件同时满足时,会产生“对象消失”的问 题,即原本应该是黑色的对象被误标为白色:

  • 赋值器插入了一条或多条从黑色对象到白色对象的新引用;
  • 赋值器删除了全部从灰色对象到该白色对象的直接或间接引用。

我们要解决并发扫描时的对象消失问题,只需破坏这两个条件的任意一个即可。由此分别 产生了两种解决方案:增量更新(Incremental Update)和原始快照(Snapshot At The Beginning, SATB)。

  • 增量更新:当黑色对象插入新的指向白色对象的引用关系时,就将这个新 插入的引用记录下来,等并发扫描结束之后,再将这些记录过的引用关系中的黑色对象为根,重新扫 描一次。这可以简化理解为,黑色对象一旦新插入了指向白色对象的引用之后,它就变回灰色对象 了。
  • 原始快照:当灰色对象要删除指向白色对象的引用关系时,就将这个要删 除的引用记录下来,在并发扫描结束之后,再将这些记录过的引用关系中的灰色对象为根,重新扫描 一次。这也可以简化理解为,无论引用关系删除与否,都会按照刚刚开始扫描那一刻的对象图快照来 进行搜索。

目前CMS是基于增量更新 来做并发标记的,G1、Shenandoah则是用原始快照来实现。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

生如夏花般绚丽

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值