g1的cleanup阶段不释放内存,仅并发读取prev_bitmap和next_bitmap统计空闲region并加入空闲列表;它依赖标记完成后的位图元数据,纯读操作、无stw、耗时极短,真正释放由后续gc分配决定。

并发清理阶段(Cleanup Phase)不释放内存,只做统计和准备
很多人误以为 Cleanup 阶段会真正“释放”老年代 Region,其实它根本不会回收任何对象——它只是在并发标记周期结束后,快速扫一遍每个 Region 的标记位图(prev_bitmap 和 next_bitmap),确认哪些 Region 已完全空闲(即存活对象数为 0),并把它们加入空闲列表。整个过程不修改堆内对象,也不移动数据,因此无需 STW。
关键点在于:Cleanup 是纯读操作,依赖的是前一轮并发标记完成后的最终标记结果。它不重新扫描引用,也不触发写屏障,所以能安全地与用户线程并发执行。
-
prev_bitmap存储上一轮 GC 结束时的存活对象快照 -
next_bitmap是本轮并发标记过程中正在更新的位图(由并发标记线程写入) - Cleanup 只比对这两个位图,若某 Region 在
next_bitmap中标记的存活字节数为 0,则判定为可回收
位图计算如何避免全堆扫描
G1 不靠遍历对象来判断 Region 是否空闲,而是直接查位图中该 Region 对应的“存活对象总大小”字段(每个 Region 在 next_bitmap 中有独立的元数据槽)。这个值在并发标记结束时已由 Final Marking 阶段原子写入,Cleanup 阶段只需一次内存读取即可下结论。
这种设计绕开了传统标记-清除中“遍历所有对象再统计”的开销,也规避了跨代引用导致的老年代扫描问题——因为 RSet(Remembered Set)已在并发标记期间完成了跨 Region 引用的汇总,位图本身已隐含了引用关系的收敛结果。
- 位图粒度是 Region 级,不是对象级;每个 Region 对应一个固定偏移的元数据项
- 位图内存布局连续,CPU 缓存友好,单次
mov指令即可加载存活字节数 - 如果 Region 包含大对象(Humongous),G1 会单独标记其 Header,不参与位图计数,Cleanup 会跳过这类 Region
为什么你看到的日志里 Cleanup 耗时极短
典型日志如 [GC pause (G1 Cleanup) (initial-mark), 0.0023456 secs],这个时间几乎全是锁竞争和元数据同步开销,不是计算耗时。真正位图读取本身在纳秒级完成。
但要注意:Cleanup 后并不会立刻把 Region 归还给操作系统(除非配置了 -XX:+AlwaysPreTouch 或 -XX:G1HeapRegionSize 较大且连续空闲足够多),它只是把 Region 标记为空闲,并加入全局空闲链表。后续 Young GC 或 Mixed GC 分配新对象时才会复用这些 Region。
- Region 是否真正“释放”取决于是否被后续 GC 选为分配目标,而非 Cleanup 行为本身
- 如果堆内存长期高位运行,Cleanup 判定为空闲的 Region 很快又被分配,看起来就像“没释放”
- 位图计算结果不可逆——一旦某个 Region 在 Cleanup 中被判定为空闲,它就不会在本次并发周期中再次进入回收集(CSet)
容易被忽略的陷阱:位图未及时刷新导致误判
极少数情况下(如系统负载极高、并发标记线程被严重抢占),next_bitmap 中某 Region 的存活字节数可能尚未写入完成,Cleanup 就已读取——此时读到的是旧值或零值,造成误判为空闲 Region。虽然概率极低,但会导致后续分配时出现 java.lang.OutOfMemoryError: GC overhead limit exceeded 或对象覆盖。
这种问题无法通过调参修复,只能靠监控识别:
- 观察日志中
[GC concurrent-mark-end]与紧接着的[GC pause (G1 Cleanup)]时间差是否异常拉长(>10ms) - 检查
-Xlog:gc+marking=debug输出中是否有Marking incomplete for region X类似提示 - 启用
-XX:+UnlockDiagnosticVMOptions -XX:+PrintAdaptiveSizePolicy查看 G1 是否因标记延迟而自动降级为 Full GC
位图的正确性依赖于并发标记线程的完成质量,而不是 Cleanup 阶段本身——这点常被调试者反向归因。










