标记-整理算法不解决扫描性能损耗,而是通过标记存活对象、向一端紧凑移动、更新引用三步消除老年代内存碎片;它间接减少full gc频次和大对象分配失败,但无法缓解卡表扫描或跨代引用开销。

标记-整理算法本身不用于“扫描”老年代变量,它解决的是老年代内存碎片问题,而非扫描性能损耗。所谓“变量存扫性能损耗”,实际指向两个常见误解:一是误以为老年代 GC 会频繁扫描所有对象字段(实则只在标记阶段遍历引用图);二是把跨代引用导致的卡表维护开销错归因于标记-整理过程。
先厘清:标记-整理不处理“扫描损耗”,它处理“空间碎片”
标记-整理(Mark-Compact)是老年代回收阶段的一种空间管理策略,核心动作只有三步:标记存活对象 → 向一端紧凑移动 → 更新所有引用。它不介入变量读写、不监听字段赋值、也不参与 Minor GC 的跨代引用扫描。因此,它无法缓解因老年代频繁修改新生代引用而引发的卡表脏页爆炸、写屏障争用等真实损耗。
真正造成“老年代变量扫描慢”的,是以下机制:
- Minor GC 时扫描老年代卡表:为找 GC Roots,必须检查所有被标记为 dirty 的卡页——脏页越多,扫描越慢;
- 老年代 GC 时遍历自身对象图:如 CMS 或 Serial Old,在并发标记或 STW 标记阶段,需逐个访问对象头、解析引用字段——若对象引用关系深、字段多(如嵌套 Map/List),标记耗时上升;
- 大对象未压缩导致分配器搜索低效:空闲内存碎成小块,每次分配都要遍历空闲链表,间接拉长 GC 准备时间。
标记-整理如何间接改善“扫描相关”体验
它虽不加速扫描本身,但通过优化内存布局,能降低扫描触发频率与范围:
- 减少 Full GC 次数:整理后腾出大块连续空间,大对象可直接分配,避免因分配失败退化为 Full GC——而 Full GC 才会真正全堆扫描,停顿极长;
- 提升晋升稳定性:碎片少 → 年轻代对象晋升更可控 → 老年代增长更平缓 → 触发 Mixed GC 或 Major GC 的节奏更可预测,避免突发性大规模扫描;
- 配合 G1 的部分整理,缩小扫描粒度:G1 不整全堆,而是选若干 Region 整理;Mixed GC 只扫描已标记为“待回收 + 有 RSet 引用”的 Region,跳过干净区域——这比传统全老年代扫描高效得多。
想降“变量扫描损耗”,该调什么而不是标记-整理
如果观察到 Minor GC 时间飙升、或 GC 日志中 G1EagerReclaimYoungRegions / Scan RS 耗时异常,说明瓶颈在跨代引用管理,不是标记-整理没跑好:
- 限制跨代写频率:避免老年代容器(如全局缓存 Map)高频 put 新生代对象;改用弱引用缓存、或批量刷新代替单条更新;
-
启用卡表优化参数:
-XX:+UseCondCardMark避免重复置脏,-XX:G1ConcRefinementThreads=4加速脏卡清理(但勿设过高,防 CPU 争抢); - 换用无卡表 GC 器:ZGC 或 Shenandoah 用读屏障+染色指针,彻底消除写屏障开销,天然规避脏卡问题;
-
监控并收缩 RSet:用
-XX:+PrintGCDetails查G1RSetScannedCards和G1RSetTotalElapsedTime,若 RSet 卡片数持续 >50 万,考虑减小 Region 大小(-XX:G1HeapRegionSize)或调高-XX:G1RSetSparseRegionEntries。
什么时候真该关注标记-整理执行效果
仅当出现以下现象时,才说明标记-整理环节本身成了瓶颈:
- 使用 Serial Old 或 Parallel Old 时,Major GC 日志中
compact阶段耗时占比超 40%; - G1 Mixed GC 中
[Ext Root Scanning]很短,但[Object Copy]或[Termination]明显拉长; - 堆中 Humongous 对象占比高(>5%),且每次整理都需移动数 MB 级对象,导致 STW 延长。
此时可:启用 -XX:+AlwaysTenure 减少晋升扰动;调大 -XX:MaxGCPauseMillis 让 G1 更倾向选小 Region 整理;或对超大对象走堆外存储(如 off-heap cache),绕过 JVM 内存管理。










