文章目录
一、并发标记与三色标记
在三色标记法之前有一个算法叫 Mark-And-Sweep(标记清除)。这个算法会设置一个标志位来记录对象是否被使用。最开始所有的标记位都是 0,如果发现对象是可达的就会置为 1,一步步下去就会呈现一个类似树状的结果。等标记的步骤完成后,会将未被标记的对象统一清理,再次把所有的标记位 设置成 0 方便下次清理。
这个算法最大的问题是 GC 执行期间需要把整个程序完全暂停,不能异步进行 GC 操作。所以就需要一个算法来解决 GC 运行时程序长时间挂起的问题,如之前在垃圾收集器中介绍的CMS及G1垃圾收集器等,在进行垃圾收集的过程中都存在并发标记的过程,其中主要采用三色标记进行处理。
三色标记最大的好处是可以异步执行,从而可以以中断时间极少的代价或者完全没有中断来进行整个 GC。 三色标记法很简单。首先将对象用三种颜色表示,分别是白色、灰色和黑色。

- 黑色: 根对象,或者该对象与它的子对象都被扫描过。
- 灰色: 对本身被扫描,但是还没扫描完该对象的子对象。
- 白色: 未被扫描对象,如果扫描完所有对象之后,最终为白色的为不可达对象,既垃圾对象。
1.1、并发标记过程中漏标问题
在并发标记过程中,垃圾收集的多线程和应用的多线程同时进行,从 GC Root 开始对堆中对象进行可达性分析,递归扫描整个堆里的对象图,找出要回收的对象,这阶段耗时较长,这时应用线程可能就会导致一些对象引用的变动,这些对象会漏标。
如在线程1完成了所有标记工作,线程2还未完成时(这时如上图),这时在线程2进行标记工作时,应用线程将对象C由对象B的引用改为对象A的引用,这时对象C就会漏标,从而被错误回收。

为解决上述漏标问题,在在垃圾收集器中介绍了CMS和G1中分别还有重新标记和最终标记的过程。
1.1.1、CMS 解决方案
CMS垃圾收集器采用了Incremental Update算法,即当一个白色对象被一个黑色对象引用,将黑色对象重新标记为灰色,让垃圾回收器重新进行扫描。
然后在重新标记过程中,对应用线程再做一次暂停,用于处理重新标记为灰色对象及其子对象的引用。
1.1.2、G1 解决方案
G1垃圾收集器采用了SATB(snapshot-at-the-beginning) 算法,刚开始做一个快照,当 B 和 C 消失的时候要把这个引用推到 GC 的堆栈,保证 C 还能被 GC 扫描到。
最重要的是要把这个引用推到 GC 的堆栈,是灰色对象指向白色的引用,如果一旦某一个引用消失掉了,我会把它放到栈(GC方法运行时数据也是来自栈中),那么其实还是能找到它的,下回直接扫描它即可,这样白色就不会漏标。
最后在最终标记过程中,对应用线程再做一次短暂的暂停,用于处理并发阶段结后仍遗留下来的最后那少量的 SATB 记录(漏标对象)。
二、G1的技术细节
2.1、跨代引用
堆空间通常被划分为新生代和老年代。由于新生代的垃圾收集通常很频繁,如果老年代对象引用了新生代的对象,那么回收新生代的话,需要跟踪从老年代到新生代的所有引用,所以要避免每次 YGC 时扫描整个老年代,减少开销。
G1垃圾收集器中主要采用了RSet(记忆集) 和 CardTable,如下:

下述介绍的是 G1 处理跨代引用的细节,其实在 CMS 中也有类似的处理方式,比如 CardTable,也需要记录一个 RSet 来记录,我们对比一下,在 G1 中是每一个 Region 都需要一个 RSet 的内存区域,导致有 G1 的 RSet 可能会占据整个堆容量的 20%乃至更多。但是 CMS 只需要一份,所以就内存占用来说,G1占用的内存需求更大,虽然 G1 的优点很多,但是一般不推荐在堆空间比较小的情况下使用 G1。
2.2、RSet(记忆集)
主要用于记录其他 Region 中的对象到本 Region 的引用。
RSet 的价值在于使得垃圾收集器不需要扫描整个堆,找到谁引用了当前分区中的对象,只需要扫描 RSet 即可。 RSet 本身就是一个 Hash 表,如果是在 G1 的话,则是在每一个 Region 区里面。
2.3、CardTable
由于做新生代 GC 时,需要扫描整个老年代,效率非常低,所以 JVM 设计了 CardTable,如果一个 老年代 CardTable 中有对象指向新生代, 就将它设为 Dirty (标志位 1),下次扫描时,只需要扫描 CardTable 上是 Dirty 的内存区域即可。
字节数组 CardTable 的每一个元素都对应着其标识的内存区域中一块特定大小的内存块,这个内存块被称作“卡页”(Card Page)。 一般来说,卡页大小都是以 2 的 N 次幂的字节数,假设使用的卡页是 2 的 10 次幂,即 1K,内存区域的起始地址是 0x0000 的话,数组 CardTable 的第 0、1、2 号元素,分别 对应了地址范围为 0x0000 ~ 0x03FF、0x0400 ~ 0x07FF、0x0800 ~ 0x011FF 的卡页内存。
三、安全点与安全区域
3.1、安全点
当用户线程暂停,GC 线程要开始工作时,首先需要确保用户线程暂停的这行字节码指令是不会导致引用关系的变化。所以 JVM 会在字节码指令中,选一些指令, 作为“安全点”,比如方法调用、循环跳转、异常跳转等,一般是这些指令才会产生安全点。
为什么它叫安全点,因为GC 时需要暂停业务线程,但并不是抢占式中断(立马把业务线程中断),而是主动是中断。 主动式中断是设置一个标志,这个标志是中断标志,各业务线程在运行过程中会不停的主动去轮询这个标志,一旦发现中断标志为true,就会在自己最近的“安全点”上主动中断挂起。
3.2、安全区域
有了上述介绍的安全点概念后,但是要是应用线程都不执行(应用线程处于 sleep 或者是 blocked 状态),那么程序就没办法进入安全点,对于这种情况,就必须引入安全区域。
安全区域是指能够确保在某一段代码片段之中, 引用关系不会发生变化,因此,在这个区域中任意地方开始垃圾收集都是安全的。我们也可以把安全区域看作被扩展拉伸了的安全点。
当应用线程执行到安全区域里面的代码时,首先会标识自己已经进入了安全区域,这段时间里 JVM 要发起 GC 就不用再去管这个线程了。 当线程要离开安全区域时,需要判断 JVM 是否已经完成了(根节点枚举,或者其他 GC 中需要暂停用户线程的阶段)
- 如果已经完成,那线程就会继续执行
- 否则它就必须一直等待, 直到收到可以离开安全区域的信号为止

本文深入探讨了HotSpot虚拟机中的并发标记算法,重点讲解了三色标记法及其在CMS和G1垃圾收集器中的应用,解决并发标记过程中的漏标问题。并介绍了G1的技术细节,包括跨代引用处理的RSet和CardTable,以及安全点与安全区域的概念。

635

被折叠的 条评论
为什么被折叠?



