g1标记位图采用prev_bitmap和next_bitmap双快照设计,gc与应用线程仅对next_bitmap执行原子置位(通过atomic::cmpxchg操作uintptr_t字),确保发布语义而非互斥更新;位图与satb日志完全解耦,应用线程不直接修改位图,仅写入satb缓冲区,由gc线程批量消费并标记,实现高并发低延迟。

标记位图的内存布局与并发访问本质
G1 的标记位图不是单个全局变量,而是两个独立的位图:prev_bitmap 和 next_bitmap,分别对应上一轮和本轮并发标记的快照。每次标记周期开始时,next_bitmap 会被清零,GC 线程和应用线程都只对它做**只写(set bit)**操作——这正是并发安全的前提:没有读-改-写(RMW)竞争。
关键点在于:标记过程不要求“原子地读取并翻转某一位”,只要求“确保某一位被设为 1 后,后续所有读操作都能看到这个 1”。这就把问题简化为**发布(publish)语义**,而非互斥更新。
Atomic::cmpxchg 在 bitmap set 中的实际调用位置
G1 并不直接对位图字节做 CAS;它先计算目标 bit 在内存中的字节偏移和位偏移,再对所在 uintptr_t 字(通常是 8 字节)执行原子 CAS。HotSpot 中实际调用的是:
Atomic::cmpxchg(new_word, (volatile uintptr_t*)addr, old_word)
其中 addr 指向该 word 的起始地址,old_word 是当前值,new_word 是将目标 bit 置 1 后的新值。失败重试逻辑由底层封装,上层无需显式循环。
- 这个 CAS 不是保护“整个位图”,而是保护“一个 word 内最多 64 个 bit”的并发修改
- 多个线程同时设置同一 word 内不同 bit,仍可能因 CAS 失败而重试,但概率极低
- 若两个线程恰好要设同一个 bit(已为 1),CAS 会失败但无害——标记幂等
为什么不用 Atomic::or 而坚持用 cmpxchg
表面上看,Atomic::or 更适合“置位”场景,但 G1 在 HotSpot 实现中统一使用 cmpxchg,原因很实在:
-
Atomic::or在某些平台(如早期 ARM)上并非原生指令,JVM 会退化为 cmpxchg 循环模拟 - 使用统一入口便于 JIT 内联和屏障插入(比如在 cmpxchg 前后自动加 acquire/release 语义)
- 避免因 or 操作隐含的读-改-写语义,在跨 NUMA 节点时引发意外缓存行争用
也就是说,选择 cmpxchg 不是因为它“更强大”,而是因为它在所有支持平台下行为最可控、最容易与 JVM 内存模型对齐。
标记位图与 SATB 日志的协作边界在哪
位图负责“记录谁被标为存活”,SATB 日志负责“记录谁本该被标但被删了”——二者完全解耦,不共享任何内存结构。
- 位图修改仅发生在 GC 线程遍历对象图、或应用线程触发 pre-write barrier 后的 SATB 日志消费阶段
- 应用线程在赋值时**从不修改位图**,只往线程本地 SATB 缓冲区 push 旧引用
- GC 线程批量消费 SATB 日志时,才对日志中每个旧引用对象调用
mark_in_bitmap,此时才真正去原子设置位图
这个分离设计让位图的并发压力集中在 GC 线程侧,而应用线程路径保持极轻量——这也是 G1 能在高吞吐场景下压低 STW 的关键隐藏约束。










