1、根节点枚举
前面讲到了可以作为根节点的对象,但如果需要去遍历寻找这些对象的话成本就太高了,并且这个过程是会造成 STW 的,因此需要一种更高效的枚举手段;
HotSpot 中采用一组 OopMap 数据结构记录对象的引用情况,在类加载完成后 HotSpot 会记录下对象中属性的偏移量和类型,在即时编译阶段会在特定位置记录下对象引用情况,OopMap 就是用来记录这两部分内容的,后面扫描时直接找到这些对象作为 GC Root 即可;
2、安全点
实际情况不可能给每个指令都创建一个 OopMap ,只有在特定位置——安全点才会去生成 OopMap,那么如何让用户线程移动到安全点暂停呢?
一种方案是检测到垃圾收集则马上暂停线程,如果有线程不在安全点上则让它继续走到上面再中断,这种方式几乎没有 JVM 在用;
另一种是在安全点处打上标记,线程不断轮询该标记的真假,待该标记为真时,线程走到最近的安全区主动暂停,完成垃圾收集工作之后线程再恢复运行。
这里轮询操作在 HotSpot 中是使用了 内存保护陷阱 方式,如果此时需要暂停用户线程则虚拟机会将轮询指令所在内存页设置为不可访问,当线程访问到这里时会产生一个自陷异常信号,在预先注册的异常处理器中挂起线程等待;
3、安全区
对于活动线程我们可以使用安全点的概念,但对于阻塞挂起的线程我们需要使用到安全区的概念,一旦线程进入安全区之后则会打上 “位于安全区”标识,此时垃圾收集就会忽略这些线程,因为他们的引用关系是不会变化的,待到他们想离开安全区时会检测垃圾收集是否已经完成,如果未完成则暂停等待出安全区的信号,完成则正常出安全区。
4、记忆集和卡表
前面讲到跨代收集问题需要使用记忆集来解决,记忆集中存储的是非收集区域对象指向收集区域对象的指针集合,最常用的实现是 卡表 方式。卡表是一个字节数组的形式,其中每个元素代表一个内存块,称为 卡页 ,如果该卡页内有任何一个对象字段存在跨代引用指针则将整个页加入 GC Root中一并扫描。
5、写屏障
写屏障的存在是为了维护卡表的状态,其相当于 AOP 操作的环绕通知,在引用类型字段赋值的前后都可以使用,在之前为 写前屏障,在之后为 写后屏障。在引用类型字段赋值之后的写后屏障中会有维护卡表状态的方法,以此达到维护记录的目的。
6、并发可达性分析
针对并发扫描出现的对象消失问题,主要解决方式有增量更新 和 原始快照SATB 。
增量更新:当对象 A 标记完成之后,用户增加了 A 引用的对象,那么会将该引用记录下来,等到并发标记结束后再以对象 A 为根去进行扫描;
原始快照:对象 B 代表已经被扫描过但至少还有一个引用没被扫描的情况,此时如果用户线程删除了对象 B 对未扫描对象 C 的引用,会导致对象 C 被错误清除,此时虚拟机为将该删除记录下来,待并发标记结束后以对象 B 为根扫描一次。