漏标会导致程序崩溃,因为将正在被业务线程使用的对象误判为垃圾并回收,引发空指针或类未找到异常;CMS用增量更新+写屏障将新引用的白色对象重标为灰色,G1则用SATB快照删除前的引用关系。

漏标为什么会导致程序崩溃,而不是只多回收点内存
漏标不是“少标几个对象”,而是把正在被业务线程使用的对象错误判定为垃圾。比如一个 OrderService 实例刚被创建并放入缓存,但并发标记时它已被标记为黑色,随后业务线程又给它新增了一个对 PaymentProcessor 的引用(白色对象),而 GC 线程此时已跳过该黑色对象——不再检查它的新引用。结果 PaymentProcessor 一直没被访问到,最终被清理。下一次业务调用时触发空指针或 NoClassDefFoundError,直接 crash。
浮动垃圾(多标)顶多浪费点内存;漏标是逻辑性破坏,不可容忍。
CMS 怎么靠增量更新(Incremental Update)堵住漏标
CMS 在并发标记阶段,一旦发现「黑色对象新增了对白色对象的引用」,就立刻把那个白色对象强行拉回灰色队列——相当于告诉 GC:“这个对象我还没扫完,得重来”。这动作由写屏障(write barrier)触发,开销小但精准。
- 典型场景:
objA.status = new Status(),其中objA已是黑色,Status是新分配的白色对象 → 写屏障拦截,将Status入灰队列 - 代价:每次赋值都要进屏障,对高频 setter 或集合 add 操作有微小性能折损
- 注意:CMS 不处理「灰色对象删掉对白色对象的引用」这种漏标路径,所以必须依赖重新标记(Remark)阶段兜底
G1 为什么选原始快照(SATB)而不是增量更新
G1 面向大堆、分区回收,更怕 STW 时间抖动。SATB 的思路相反:不拦新增引用,而是「在引用被删除前,先快照一份旧关系」。它在写屏障中记录下即将消失的引用(比如 objB.field = null 触发时,把原 field 指向的对象记入 satb_mark_queue),后续重新标记阶段再扫描这些“曾经活过”的对象。
- 优势:写屏障只在引用被覆盖/置 null 时触发,比 CMS 的每次写都拦截更轻量
- 代价:会多标一些对象(即浮动垃圾变多),但 G1 本来就要做混合回收,影响可控
- 关键配置:
-XX:+G1UseSATBBarrier默认开启,禁用它等于放弃 G1 并发标记正确性
实操中容易忽略的两个硬约束
无论 CMS 还是 G1,三色标记能成立的前提是:GC Roots 枚举必须 STW 完成,且初始标记后不能再有新 Root 出现。这意味着:
-
ThreadLocal变量若在并发标记中被新线程首次访问并初始化,其关联对象可能逃逸标记 → 必须避免在 GC 期间动态注册线程或初始化 ThreadLocal - JNI 全局引用(
jobject globalRef)如果在并发标记中被新创建,也不会被扫描到 → JNI 层需确保所有全局引用在初始标记前就位
这些不是配置能解决的,是代码生命周期和 GC 阶段的隐式耦合,出问题时很难定位到根源。

















