根节点枚举
在枚举根节点时,所有的用户线程都会被被暂停,因为在根节点枚举过程中,为了保证分析结果的准确性,需要保证根节点的引用关系不会发生变化。即根节点的枚举必须在一个能保障内存一致性的快照中。
为了避免在查找引用链的过程中从上到下一个不漏的检查所有执行完的上下文和全局引用的位置这一耗时耗力的情况出现,在类加载完成后, HotSpot 会把对象内什么偏移量上是什么数据类型全部计算出来,并将引用在栈里和寄存器里的位置记录在 OopMap 中。
安全点
OopMap 虽然会帮助 HotSpot 迅速完成根节点枚举,但是 HotSpot 并不会为了每一条指令都生成一个 OopMap,因为这样会耗费大量的内存空间。HopSpot 只会在关键的节点生成 OopMap,用来记录引用信息,这些关键的节点称之为 安全点。
在进行垃圾收集时,要求必须执行到安全点处才可以暂停。
安全点的设定既不能使收集器等待太长时间,也不能过于频繁,过于频繁会增大系统负担。
安全点出的中断方式:
抢先中断 当发生垃圾收集时,会要求线程首先中断,然后看线程是否在安全点上,如果有线程不在安全点上,则让该线程继续执行,过一会再中断,直到该线程到达安全点上
主动中断 设立一个标志位,让运行中的线程主动轮询该该标志,一旦返现该中断标志为真时,则会在最近的安全点上自动挂起。
安全区域
对于运行中的线程可以使之到达安全点,但是对于休眠或者阻塞起来的线程则无法使之运行到达安全点。此时 HotSpot 便引入的安全区域。
安全区域是指能确保在某一段代码片段之中,引用关系不会发生变化,因此在该区域内,任意地方开始的垃圾收集是安全的。
当线程执行到安全区域时会首先标识自己进入到了安全区域,这样在垃圾收集时就不会管在安全区域里面的线程了。
在枚举根节点时,安全区域里的线程无法离开安全区域。
记忆集与卡表
记忆集是一种用于记录从非收集区域指向收集区域的指针的数据集合。记忆集的出现是为了解决对象跨代引用所带来的问题。
在垃圾收集时,只需要判断某一块非收集区域的内存是否含有指向收集区域的内存即可。也就是说记忆集并非要将每一个指针都记录下来,因此记忆集也有精度之分。
记忆集的精度有:
- 字长精度:每个记录都精确到每一个机器字长,即该物理内存的地址包含跨代指针。
- 对象精度:每个记录都精确到一个对象,即该对象中含有跨代指针。
- 卡精度: 每个记录精确到一块内存区域,该区域内含有跨代指针。
使用卡精度的记忆集称之为卡表,被映射的一块内存区域称之为卡页。
卡表元素的维护与写屏障
在有其他分区元素引用该分区对象时,卡表元素就会变脏,变脏的时间点就在引用字段类型赋值的那一刻。
卡表元素维护的方式是写屏障,写屏障可以看做是虚拟机层面对于“引用字段类型赋值”操作的 AOP 切面,赋值前的动作称为“写前屏障”,复制后的动作称为“写后屏障”。
并发可达性
在根节点枚举结束后,接下来要做的就是从根节点遍历对象图,而该阶段会和用户线程并发运行。
无论采用什么样的算法都是要对对象进行标记的,一般采用三色标记法。该方法依据是否访问过对象来对对象进行标记:
- 白色:表示对象未被垃圾收集器访问过
- 黑色:表示该对象已被垃圾收集器访问过且该对象内所有的引用均被扫描过
- 灰色:该对象被垃圾收集器访问过,且该对象内的所有引用至少有一个没有被扫描过
一般来说,经历过一次遍历后,标记为白色的对象便是需要回收的对象,但是由于遍历过程是与用户线程并发运行的,用户线程会在运行过程中修改对象引用的关系,这样便会导致“对象消失” 问题,即将原本已存活的对象标记为消亡。
将存活的对象标记为消亡,其实就是本应为黑色的对象被标记为白色,导致该情况发生的步骤一般有两个:
- 赋值器插入了一条或多条从黑色对象到白色对象的新引用
- 赋值器删除了所有从灰色对象到白色对象的直接或间接引用。
解决“对象消失” 的方法:
- 增量更新:当黑色对象插入新的指向白色对象的引用关系时,就要将这个新插入的引用记录下来,待并发扫描结束后,再将这些记录过的,引用关系为黑色对象的根,重新扫描一遍。也可以说是将这些黑色对象变为灰色对象重新扫描。
- 原始快照:当灰色节点指向白色对象的引用被删除时,将被删除的引用记录下来,在并发扫描结束后,从该引用开始,重新进行扫描。相当于将被删除的白色节点记录下来,待扫描结束后,将白色节点作为根,将白色节点变成灰色节点,开始扫描。