预清理是cms垃圾回收中为缩短remark阶段stw时间的关键前置步骤,它通过仅扫描脏卡、提前重标记、识别浮动垃圾及等待young gc来减轻remark负担。

重新标记(Remark)阶段本身必须 STW,因为它要精确修正并发标记期间因用户线程持续运行导致的引用变动——比如老年代对象字段被修改、新生代对象晋升并引用老年代等。这些变动若全留到 Remark 才处理,就得扫描整个堆(尤其年轻代对老年代的跨代引用),STW 时间会显著拉长。
预清理是为 Remark “减负”的关键前置动作
它不直接减少 Remark 的必要性,而是把 Remark 中本该做的大量工作提前在并发线程中完成:
- 只扫描“脏卡”(Dirty Card):写屏障机制已记录下并发标记期间被修改的老年代内存页,预清理专注处理这些区域,避免全堆扫描
- 提前重新标记受影响对象:例如老年代 A 对象在并发阶段被赋值指向新晋升的 B 对象,预清理就顺着这条新引用把 B 及其可达对象标记上
- 识别并丢弃浮动垃圾:那些在并发标记后已变为不可达、但尚未被清除的对象,预清理可提前判定并跳过,减少 Remark 校验量
- 主动等待 Young GC:可中断预清理阶段会暂停自身、等待一次 Minor GC 发生;一旦触发,年轻代变干净,Remark 就无需再花时间遍历大量新生代引用
为什么不能跳过预清理、全靠 Remark 解决?
因为 Remark 是 STW 阶段,所有用户线程必须停摆。若没有预清理,每次 Remark 都要:
- 扫描全部年轻代存活对象,检查它们对老年代的引用
- 遍历所有可能被并发修改的老年代区域,而非仅限脏卡
- 重复验证大量已在并发阶段确认存活的对象
这会使 STW 时间从毫秒级升至百毫秒甚至更久,违背 CMS “低延迟”设计初衷。
实战中预清理效果取决于触发时机和压力控制
预清理是否有效,不看它做了多少,而看它是否来得及做:
- 设 -XX:CMSInitiatingOccupancyFraction=50,让 CMS 更早启动,避免老年代快满时才开始预清理、脏卡堆积过多
- 避免老年代高频写:如单一大 ConcurrentHashMap 持续 put/remove,会频繁触发写屏障、生成大量脏卡
- 启用 -XX:+CMSScavengeBeforeRemark,强制在 Remark 前做一次 Young GC,进一步降低跨代引用扫描负担
日志里频繁出现 Abortable Preclean interrupted,不是预清理失败,而是它已主动让出时间——说明系统压力大或触发太晚,此时 Remark 的 STW 很可能已经变长。
Java免费学习笔记:立即使用
解锁 Java 大师之旅:从入门到精通的终极指南











