1 根节点枚举
我们以可达性分析算法中从GC Roots集合找引用链这个操作作为介绍虚拟机高效实现的第一个例 子。固定可作为GC Roots的节点主要在全局性的引用(例如常量或类静态属性)与执行上下文(例如 栈帧中的本地变量表)中,尽管目标明确,但查找过程要做到高效并非一件容易的事情。现在可达性分析算法耗时 最长的
查找引用链的过程
已经可以做到与用户线程一起并发。但根节点枚举始终还 是必须在一个能保障一致性的快照中才得以进行——这里“一致性”的意思是整个枚举期间执行子系统 看起来就像被冻结在某个时间点上,不会出现分析过程中,根节点集合的对象引用关系还在不断变化 的情况,若这点不能满足的话,分析结果准确性也就无法保证。**
这是导致垃圾收集过程必须停顿所有 用户线程的其中一个重要原因
,即使是号称停顿时间可控,或者(几乎)不会发生停顿的CMS、G1、 ZGC等收集器,
枚举根节点时也是必须要停顿的
。(为什么停顿?若这点不能满足的话,分析结果准确性也就无法保证。)**
用户线程停顿下来之后,其实并不需要一个不漏地检查完所有 执行上下文和全局的引用位置,虚拟机应当是有办法直接得到哪些地方存放着对象引用的。在HotSpot 的解决方案里,是使用一组称为OopMap的数据结构来达到这个目的。
2 安全点 安全区域
实际上HotSpot也的确没有为每条指令都生成OopMap,只是在**“特定的位置”记录 了这些信息,这些位置被称为安全点(Safepoint)**。有了安全点的设定,也就决定了用户程序执行时 并非在代码指令流的任意位置都能够停顿下来开始垃圾收集,而是强制要求必须执行到达安全点后才 能够暂停。当用户线程执行到安全区域里面的代码时,首先会标识自己已经进入了安全区域,那样当这段时 间里虚拟机要发起垃圾收集时就不必去管这些已声明自己在安全区域内的线程了。当线程要离开安全 区域时,**它要检查虚拟机是否已经完成了根节点枚举(**或者垃圾收集过程中其他需要暂停用户线程的 阶段),如果完成了,那线程就当作没事发生过,继续执行;否则它就必须一直等待,直到收到可以 离开安全区域的信号为止。
3 记忆集与卡表
讲解分代收集理论的时候,提到了为解决对象跨代引用所带来的问题,垃圾收集器在新生代中建 立了名为记忆集(Remembered Set)的数据结构,用以避免把整个老年代加进GC Roots扫描范围。事 实上并不只是新生代、老年代之间才有跨代引用的问题,所有涉及部分区域收集(Partial GC)行为的 垃圾收集器,典型的如G1、ZGC和Shenandoah收集器,都会面临相同的问题。记忆集是一种用于记录从非收集区域指向收集区域的指针集合的抽象数据结构。
4 写屏障(AOP)
解决卡表元素如何维 护的问题,例如它们何时变脏、谁来把它们变脏等。有其他分代区域中对象引用了本区域对象时,其对应的 卡表元素就应该变脏,变脏时间点原则上应该发生在引用类型字段赋值的那一刻。在HotSpot虚拟机里是通过写屏障(Write Barrier)技术维护卡表状态的。先请读者注意将这里提 到的“写屏障”,以及后面在低延迟收集器中会提到的“读屏障”与解决并发乱序执行问题中的“内存屏障” [1] 区分开来,避免混淆。写屏障可以看作在虚拟机层面对**“引用类型字段赋值”这个动作的AOP切 面** [2] ,在引用对象赋值时会产生一个环形(Around)通知,供程序执行额外的动作,也就是说赋值的前后都在写屏障的覆盖范畴内。在赋值前的部分的写屏障叫作写前屏障(Pre-Write Barrier),在赋值 后的则叫作写后屏障(Post-Write Barrier)。HotSpot虚拟机的许多收集器中都有使用到写屏障,但直 至G1收集器出现之前,其他收集器都只用到了写后屏障。
这个语境上的内存屏障(Memory Barrier)的目的是为了指令不因编译优化、CPU执行优化等原因 而导致乱序执行,它也是可以细分为仅确保读操作顺序正确性和仅确保写操作顺序正确性的内存屏障 的。关于并发问题中内存屏障的介绍,可以参考volatile型变量。
5 并发的可达性分析
想解决或者降低用户线程的停顿,就要先搞清楚为什么必须在一个能保障一致性的快照上才能进行对象图的遍历?为了能解释清楚这个问题,我们引入三色标记(Tri-color Marking) [1] 作为工具来辅 助推导,把遍历对象图过程中遇到的对象,按照“是否访问过”这个条件标记成三种颜色。
Wilson于1994年在理论上证明了,当且仅当以下两个条件同时满足时,会产生“对象消失”的问 题,即原本应该是黑色的对象被误标为白色:
- ·赋值器插入了一条或多条从黑色对象到白色对象的新引用;
- ·赋值器删除了全部从灰色对象到该白色对象的直接或间接引用。 因此,我们要解决并发扫描时的对象消失问题,只需破坏这两个条件的任意一个即可。由此分别 产生了两种解决方案:增量更新(Incremental Update)和原始快照(Snapshot At The Beginning, SATB)。
增量更新要破坏的是第一个条件,当黑色对象插入新的指向白色对象的引用关系时,就将这个新 插入的引用记录下来,等并发扫描结束之后,再将这些记录过的引用关系中的黑色对象为根,重新扫 描一次。这可以简化理解为,黑色对象一旦新插入了指向白色对象的引用之后,它就变回灰色对象 了。
原始快照要破坏的是第二个条件,当灰色对象要删除指向白色对象的引用关系时,就将这个要删 除的引用记录下来,在并发扫描结束之后,再将这些记录过的引用关系中的灰色对象为根,重新扫描 一次。这也可以简化理解为,无论引用关系删除与否,都会按照刚刚开始扫描那一刻的对象图快照来 进行搜索。以上无论是对引用关系记录的插入还是删除,虚拟机的记录操作都是通过写屏障实现的。在 HotSpot虚拟机中,增量更新和原始快照这两种解决方案都有实际应用,譬如,CMS是基于增量更新 来做并发标记的,G1、Shenandoah则是用原始快照来实现。