首先了解一下三色标记法的三种颜色:

  • 白色:表示对象尚未被垃圾收集器访问过。显然在可达性分析刚刚开始的阶段,所有的对象都是白色的,若在分析结束的阶段,仍然是白色的对象,即代表不可达。
  • 黑色:表示对象已经被垃圾收集器访问过,且这个对象的所有引用都已经扫描过。黑色对象代表已经扫描过,它是安全存活的,如果有其他对象引用指向了黑色对象,无须重新扫描一遍。黑色对象不可能直接(不经过灰色对象)指向某个白色对象。
  • 灰色:表示对象已经被垃圾收集器访问过,但这个对象上至少存在一个引用还没有被扫描过

根据可达性分析算法,从GC Root开始进行遍历,然后就可以知道哪些对象是存活的,哪些对象是不可达的(需要被垃圾回收的)。
GC Root的对象包括:

  • 虚拟机栈中引用的对象
  • 方法区中类静态属性引用的对象
  • 方法区中常量引用的对象
  • 本地方法栈中JNI(Native方法)引用的对象
  • Java虚拟机内部引用的对象(如线程对象、类加载器对象、synchronized引用的对象等)

由于存在某些阶段(如并发标记阶段),会出现用户线程和GC线程并发执行的情况,所以在标记阶段,可能会出现用户线程修改了对象引用的情况,使得三色标记法出现多标或者漏标的问题。

  • 多标
    多标
    D -> E的引用断开之后,E、F、G三个对象不可达,应该要被回收的。然而因为E已经变为灰色了,其仍会被当作存活对象继续遍历下去。最终的结果是:这部分对象仍会被标记为存活,即本轮GC不会回收这部分内存。

这部分本应该回收但是没有回收到的内存,被称之为浮动垃圾(float garbage)。浮动垃圾虽然不会影响应用程序的正确性,但是需要等到下一轮垃圾回收中才被清除。

  • 漏标
    漏标
    假设GC线程已经遍历到E(变为灰色了),然后用户线程执行了某段代码,让E断开对G的引用,D引用G,此时切回到GC线程,因为 E已经没有对G的引用了,所以不会将G置为灰色;尽管因为D重新引用了G,但因为D已经是黑色了,不会再重新做遍历处理。最终导致的结果是:G会一直是白色,最后被当作垃圾进行清除。相比于多标,漏标直接影响到了应用程序的正确性,是不可接受的。
    可以知道,当且仅当以下两个条件同时满足时,会产生“对象消失”的问题,即原本应该是黑色的对象被误标为白色:
  1. 赋值器插入了一条或多条从黑色对象到白色对象的新引用。
  2. 赋值器删除了全部从灰色对象到该白色对象的直接或间接引用。

三色标记法虽然可以减少STW的时间,但是也会引入多标和漏标的问题。由于漏标会影响应用程序的正确性,所以我们需要想办法解决漏标的问题。要解
决并发扫描时的漏标问题,只需破坏这两个条件的任意一个即可。由此分别产生了两种解决方案:增量更新(Incremental Update)和原始快照(Snapshot At The Beginning, SATB)。

增量更新
针对新增的引用,将其记录下来等待遍历,即增量更新(Incremental Update)。

增量更新破坏了漏标的条件一:【 一个或者多个黑色对象重新引用了白色对象】,从而保证了不会漏标。

原始快照
尝试保留开始时的对象图,即原始快照(Snapshot At The Beginning,SATB),当某个时刻的GC Roots确定后,当时的对象图就已经确定了。比如当时 E 是引用着 G 的,那后续的标记也应该是按照这个时刻的对象图走(E 引用着 G)。如果期间发生变化,则可以记录起来,保证标记依然按照原本的视图来。

原始快照破坏了漏标的条件二:【灰色对象断开了白色对象的引用(直接或间接的引用)】,从而保证了不会漏标。