老年代GC慢是Mark-and-Compact算法在高存活率下必然的线性开销:需串行完成标记、计算新地址、移动对象并修正引用,且后两步STW,耗时与存活对象数成正比;复制算法因内存成本过高不可行,标记-清除则引发碎片,故该延迟是算法约束下的物理事实。

老年代 GC 慢不是“偶尔慢”,而是 Mark-and-Compact 算法在对象高存活率场景下必然产生的线性开销放大——它不光要标记,还要移动、重写引用、整理地址空间。
Mark-and-Compact 的三阶段耗时来源
年轻代用复制算法(如 ParNew),只复制存活对象;而老年代的 Mark-and-Compact 必须完成三个不可省略的串行步骤:
- 标记所有存活对象(需遍历整个老年代 + 扫描跨代引用,比如从年轻代指向老年代的引用)
- 计算每个存活对象的新地址(按内存顺序紧凑排列,需累加大小并维护偏移)
- 实际移动对象 + 修正所有指向它的引用(包括栈帧、寄存器、其他堆对象中的字段)
其中第 2、3 步必须 STW(Stop-The-World),且耗时与 存活对象数量成正比。当老年代存活率常达 70%~95%,这些步骤的开销远超年轻代复制 5%~10% 存活对象的成本。
为什么不能像年轻代那样“只复制”?
复制算法要求预留一块完整空闲空间(如 Survivor 区),但老年代通常占堆 60%~80%,再划出同等大小空闲区不现实。于是:
- 若强行用复制,需额外分配等于老年代大小的内存——直接翻倍堆需求,多数生产环境无法接受
- 标记-清除(Mark-Sweep)虽省空间,但碎片化后无法分配大对象,触发频繁 Full GC
-
Mark-and-Compact是折中:用时间换空间,通过移动解决碎片,但代价是每次回收都必须重排全部存活对象
这就是“10 倍以上”延迟的根源:不是 JVM 懒,而是算法约束下的物理事实。
并发标记并不能抵消 Compact 阶段的停顿
CMS 或 G1 的并发标记阶段确实和用户线程并行,但关键的 Compact(或 Evacuation)阶段仍需 STW:
- CMS 实际已废弃,其
concurrent mode failure会退化为单线程MSC(Mark-Sweep-Compact),全程 STW,耗时爆炸 - G1 的 Mixed GC 中,
Evacuation阶段本质是带目标区域的复制,但它仍需暂停来更新 Remembered Set 和 RSet 引用,且受限于可用 Humongous 区/Region 数量 - ZGC/Shenandoah 虽标榜低延迟,但它们的“移动”逻辑被拆解为读屏障+并发转移,仍存在小幅度的元数据更新暂停,并非零开销
也就是说:只要涉及对象移动和引用修正,就绕不开某种形式的同步协调成本。
容易被忽略的隐性放大因素
除了算法本身,还有几个常被低估的放大器:
- 老年代越大,标记阶段遍历的卡表(Card Table)或记忆集(Remembered Set)条目越多,间接增加标记时间
- 跨代引用(如年轻代对象持有老年代对象引用)需额外扫描,JVM 用卡表粗筛,但漏标会导致重新标记(Remark),延长 STW
- 对象越老,其引用链往往越深(如缓存容器、单例管理器、静态集合),导致标记深度增加、缓存不友好
- Compact 后对象内存地址变更,CPU 缓存行大量失效,后续业务代码首次访问时出现密集 cache miss
这些细节不会出现在 GC 日志的 “user” 时间里,却实实在在拖慢了整体响应——这也是调优时只看 GC pause time 容易误判的原因。

















