标记-清除算法的标记阶段从GC Roots开始,包括虚拟机栈中局部变量、方法区静态变量与常量、JNI引用的对象;通过可达性分析(DFS/BFS)遍历引用链标记所有可达对象,为清除阶段提供依据。

标记过程是标记-清除算法的起点和关键,它决定了哪些对象能被保留、哪些会被回收。这个阶段不释放任何内存,但直接影响后续清除的准确性和效率。
标记从哪里开始:GC Roots 的真实构成
标记不是随机扫描,而是严格从一组可信的“根”出发。这些根对象本身不依赖其他对象存活,包括:
- Java 虚拟机栈中每个线程的栈帧里的局部变量、方法参数(正在执行的方法所引用的对象)
- 方法区中类的静态字段(static 修饰的变量)所引用的对象
- 方法区中的常量池里字符串常量、类字面量等直接引用的对象
- 本地方法栈中 JNI(Native 方法)持有的 Java 对象引用
注意:这些根对象必须是“活跃”的——比如一个已结束的方法,其栈帧早已弹出,对应的局部变量就不再算作 GC Roots。
标记如何传播:可达性分析的遍历逻辑
从 GC Roots 出发后,标记器会沿着对象之间的引用链逐层推进。核心是判断“是否可达”,而非“是否还有引用”。典型实现采用广度优先(BFS)或深度优先(DFS),以避免递归过深或队列爆炸:
- 初始时,所有 GC Roots 入队(或入栈),并立即标记为“已访问”
- 每次取出一个对象,检查它的每个字段:若字段是非 null 引用,且该引用指向的对象尚未标记,则标记它,并加入待处理队列
- 重复该过程,直到队列为空——此时所有从根出发可到达的对象均已标记
例如:A → B → C,且 A 是 GC Root。标记器先标 A,再通过 A 找到 B 并标记,再通过 B 找到 C 并标记;若 D 只被 C 引用而 C 已不可达,则 D 不会被标记,即使它自身仍有字段。
标记怎么记录:三种常见技术及其取舍
标记信息需要持久化,不同 JVM 实现采用不同策略:
- 对象头位图(Bit Map in Object Header):在对象头中复用少量标志位(如 mark word 的某一位),开销最小,但需对象结构支持,且并发标记时需原子操作保障安全
- 独立标记位图(Separate Bitmap):维护一块与堆内存一一映射的位图区域(1 bit 表示 1 个对象或 1 个内存单元),适合大堆和并发场景,但额外占用内存
- 标记栈/队列 + 状态字段:不依赖底层结构,用额外数据结构管理状态,灵活性高,但引入间接引用和内存分配开销
现代 JVM(如 ZGC、Shenandoah)倾向使用独立位图,兼顾并发性与可预测性;而 Serial、Parallel Old 则多采用对象头标记,追求低延迟和简单性。
标记阶段的现实约束与风险点
标记看似简单,但在实际运行中面临几个隐性挑战:
- STW 必然性:为保证对象图静止(避免用户线程修改引用关系导致漏标或错标),绝大多数标记阶段需暂停应用线程(Stop-The-World)
- 漏标问题:若标记过程中用户线程将新引用写入已标记对象,而该新引用指向未标记对象,就可能漏标——三色标记法正是为解决此问题而设计
- 精度边界:标记只识别强引用;软引用、弱引用、虚引用的对象虽可能被回收,但它们本身仍可能在标记阶段被临时视为“存活”,需后续阶段按引用强度分别处理
因此,一次成功的标记,不仅是“走完引用链”,更是对并发一致性、内存模型和引用语义的综合把控。

















