1、根节点枚举

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

对象总是在不停变化的。

1.1 安全点

基于上述的事实,维护这样一个 OopMap 是很困难的,或者说对于内存开销很大,在针对于垃圾收集的场景下是不能被接受的资源消耗。因此引出了一个安全点的概念(一些特定进行 OopMap 维护的代码指令),安全点位置的选取基本上是以“是否具有让程序长时间执行的特征”为标准进行选定的,其最明显特征就是指令序列的复用,例如方法调用、循环跳转、异常跳转等,所以只有具有这些功能的指令才会产生安全点。

在 gc 发生时,所有用户线程都需要暂停在安全点位置上,来进行引用对象的计算,这里有两种方式:

  • 抢先式中断:系统首先把所有用户线程全部中断,如果发现有用户线程中断的地方不在安全点上,就恢复这条线程执行,让它一会再重新中断,直到跑到安全点上;

  • 主动式中断:设置一个标志位,各个线程执行过程时会不停地主动去轮询这个标志,一旦发现中断标志为真时就自己在最近的安全点上主动中断挂起。

1.2 安全区

活跃线程可以主动或者被动的被挂到安全点上,但是未执行的线程无法进行通信响应,也就无法到达安全点,为此需要引入安全区域的概念(确保在某一段代码片段之中,引用关系不会发生变化)。

线程在进入安全区域时会标记自身,gc 在检查时会跳过检查,当离开时需要在 STW 的之外,否则需要一直等待,直到收到可以离开的请求。

2、记忆集&卡表

记忆集是一种用于记录从非收集区域指向收集区域的指针集合的抽象数据结构(常用于解决跨代引用带来的问题,避免把其他代全部纳入进 GC Roots 扫描范围)。

  • 记录非收集区中所有跨代引用对象标记:

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

    对象精度:每个记录精确到一个对象,该对象里有字段含有跨代指针;

    卡精度:每个记录精确到一块内存区域,该区域内有对象含有跨代指针。

记忆集是一种抽象的概念,具体实现如卡表,卡表记录的内存区域中,如果有一个对象存在跨代引用,则当前区域被标记为 1,否则是标记为 0,在垃圾收集发生时,只要筛选出卡表中变脏的元素,就能轻易得出哪些卡页内存块中包含跨代指针,把它们加入 GC Roots 中一并扫描。

3、写屏障

3.1 卡表记录变脏

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

  • 如何变脏:在引用对象赋值时会产生一个环形通知,类似于切片的方式,赋值之后去更新卡表,由虚拟机来为所有赋值操作生成相应的指令。

上述提到的环形通知就是写屏障,其前后又可以分为写前屏障和写后屏障,更新动作在写后屏障中进行。

3.2 伪共享问题

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

为此,HotSpot 虚拟机增加了一个新的参数 -XX:+UseCondCardMark,在进行更新之前先判断当前卡表是否已经变脏,如果变脏则不进行处理,虽然能够避免伪共享的问题,但是也会带来一次判断的性能开销。

4、并发性可达性分析

4.1 三色标记

  • 白色:表示对象尚未被垃圾收集器访问过;

  • 黑色:表示对象已经被垃圾收集器访问过,且这个对象的所有引用都已经扫描过;

  • 灰色:表示对象已经被垃圾收集器访问过,但这个对象上至少存在一个引用还没有被扫描过。

在并发扫描的过程中,如果同时出现了,赋值器插入了一条或多条从黑色对象到白色对象的新引用,且赋值器删除了全部从灰色对象到该白色对象的直接或间接引用,会导致本来应该存活的对象被标记为死亡,从而造成程序错误。(由 wilson 于 1994 年在理论上证明)

4.2 并发问题解决

  • 增量更新(Incremental Update):

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

  • 原始快照(Snapshot At The Beginning,SATB)。当灰色对象要删除指向白色对象的引用关系时,就将这个要删除的引用记录下来,在并发扫描结束之后,再将这些记录过的引用关系中的灰色对象为根,重新扫描一次。