预清理阶段不清理对象,而是扫描并处理“脏卡”中记录的并发标记期间被修改的老年代内存区域,以减少重标记(remark)的stw时间;其可中断设计(默认5秒或eden达50%即退出)旨在避免拖慢cms周期,频繁中断表明老年代压力大或cms触发过晚。

CMS并发预清理阶段不解决变量引用关系变动本身,而是为重标记阶段“减负”——它处理的是这些变动引发的后续影响,核心目标是减少重标记(Remark)的STW时间。
预清理到底在“清理”什么?
它清理的不是对象,而是“脏卡”(Dirty Card)中记录的、在并发标记期间被修改过的老年代内存区域。这些修改包括:
- 老年代对象字段被赋值,指向另一个老年代或新生代对象
- 集合类(如HashMap、ArrayList)在老年代对象中新增/更新元素
- 数组元素被重新赋值,且该数组位于老年代
每次这类写操作触发写屏障,对应卡页会被标记为“脏”。预清理阶段就扫描这些脏卡,重新标记其中的对象及其可达路径,把部分本该留到Remark做的工作提前完成。
为什么需要“可中断”的预清理?
因为它的耗时不可控。若放任它一直扫,可能拖慢整个CMS周期,甚至错过最佳回收时机。所以JVM设计了concurrent-abortable-preclean阶段:
- 默认最多执行5秒,或Eden区使用率升至50%就主动退出
- 若频繁看到日志中Abortable Preclean interrupted,说明老年代压力大、晋升快,或CMS触发太晚
- 这不是失败,而是策略性让步——把剩余工作留给Remark,但代价是STW时间可能变长
实战中怎么降低预清理压力?
关键不在调参数,而在控源头:
- 压低CMS触发阈值:设-XX:CMSInitiatingOccupancyFraction=50,让预清理更早介入,避免脏卡堆积如山
- 减少老年代高频写操作:避免在单个大缓存对象(如ConcurrentHashMap)中反复put/remove;改用弱引用缓存或按需重建
- 配合CMSScavengeBeforeRemark:虽不直接影响预清理,但能减少新生代跨代引用数量,间接降低预清理需关注的“新晋升+新引用”组合场景
怎么看预清理是否健康?
打开-XX:+PrintGCDetails后,关注日志中的三段:
- [CMS-concurrent-preclean: X.XXX/X.XXX secs] —— 基础预清理耗时,应稳定在毫秒级
- [CMS-concurrent-abortable-preclean: Y.YYY/Y.YYY secs] —— 可中断阶段,若接近5秒或频繁中断,需调优
- [Times: user=Y.YY sys=0.ZZ, real=Y.YY secs] 中real时间显著增长,说明CPU争抢或卡表扫描量过大
预清理本身不修正引用变动,它只是把变动带来的“标记滞后”问题分摊出去。真正兜底的,仍是重标记阶段的STW扫描。











