并发扫码带来的问题:无法判断对象到底是存活还是死亡

66 阅读3分钟

一、与用户线程并发带来的问题

我们思考下面两种场景:

1.当用户新增了一个扫描完为存活对象A到一个未扫描或者扫描完为死亡的对象B的引用。当我们结束扫描时就会发现B漏扫了。在扫描结束的那一刻,B应该是存活的,缺被当做死亡对象了。

2.当B对象未被扫描,同时用户删除了所有对象到B的引用。当我们开始扫描时,B是存活的,当我们扫描结束后,发现B是死亡的,所以对于B对象来说,在开始是存活的,但是在结束时是死亡的。

上述两种场景都会导致对象误判,我们破坏其中一条时,都只能解决对象当时是存活或者未来是存活的问题,所以在实践场景中,我们需要同时解决这2中场景,使用增量更新,记录新增的引用来解决对象扫描结束时是存活的,却被我们判定为死亡的问题。使用原始快照SATB记录扫描期间对象删除了原有的引用,解决扫描开始时对象是存活的,当扫描完后却被我们判定为死亡的问题。

image.png

二、三色标记法

如果对象存活我们用黑色标记,如果对象死亡我们用白色标记,这在停顿用户线程的扫描中没有任何问题,当我们与用户线程一起并发的时候,用户线程会修改引用,我们已经扫描完为黑色或者白色的对象可能因为用户线程的引用关系修改,需要再次扫描来判定对象是存活还是死亡,但是用户线程一直在执行,有可能我们刚扫描完,用户又改了,所以我们将用户在扫描阶段并发修改的引用对象标记出来(灰色),当我们从root根深度递归扫描完成后,再将用户线程停止,这样我们就可以安心的处理用户线程在我们扫描阶段做出的新的引用变化,也就是被灰色标记的对象。

1.三色标记法的核心概念:三种颜色的定义

三色标记法将对象分为三种状态(颜色),通过颜色的动态变化追踪对象的存活状态:

颜色含义初始状态
白色未被垃圾回收线程访问过的对象(可能是垃圾,也可能是未被扫描的存活对象)所有对象初始为白色
灰色已被垃圾回收线程访问过,但至少有一个子对象未被扫描(引用路径上的所有对象都是子对象)(需要进一步处理)GC Roots直接引用的对象
黑色已被垃圾回收线程访问过,且所有子对象都已被扫描(存活状态确认)灰色对象处理后的最终状态

2.三色标记法的执行过程

2.1默认所有对象都是白色

2.2初始标记,将gc root根对象标记为灰色 2.3从根开始,深度递归法,找到引用的叶子节点对象,将对象标记为黑色,同时会先将经过的非叶子节点对象标记为灰色

2.4黑色从叶子节点往非叶子节点染色,直到将所有gc root根对象染成黑色

3.用户并发更新的引用,通过写屏障,标记对象为灰色

flowchart TB
    subgraph 用户线程
        F[修改对象引用] --> G{写屏障触发}
        G -->|修改黑色对象| H[将黑色对象变灰]
        G -->|新增引用| I[记录新引用关系]
    end

4.最终标记

停止用户线程,处理用户新增的引用和删除的引用

flowchart TB
    subgraph GC线程
        A[从灰色队列取对象] --> B[扫描对象引用]
        B --> C[将引用的白色对象标记为灰色]
        C --> D[当前对象标记为黑色]
        D --> E{灰色队列空?}
        E -->|否| A
    end