标记-清除算法存在效率低(两次全堆遍历导致长STW)和内存碎片(空闲空间不连续致大对象分配失败)两大硬伤;复制算法虽高效无碎片,但依赖高死亡率前提,存活对象多时易触发担保失败并加重老年代压力;标记-整理算法通过移动对象解决碎片,却带来引用更新开销与NUMA架构下的非本地访问延迟;分代设计虽优化整体性能,但引入跨代引用检查、晋升策略僵化及回收节奏不同步等隐性负担。

JVM 垃圾回收算法不是万能的,每种主流算法都带着明确的取舍和现实约束。理解这些局限性,才能避免在调优或排查问题时掉进“以为GC能自动解决一切”的误区。
标记-清除:碎片与停顿难以兼顾
它最直观,但两个硬伤始终存在:一是两次全堆遍历(标记+清除),堆越大,Stop-The-World 时间越长;二是不移动对象,导致内存碎片堆积。哪怕空闲总量充足,一个 4MB 的数组也可能因找不到连续空间而直接触发 Full GC,甚至抛出 OutOfMemoryError——这不是内存不够,是“散装空闲”拼不出整块。
- 无法通过增加堆大小缓解碎片问题,反而可能加剧分配失败频率
- 现代 JVM 几乎不单独用纯标记-清除,只作为老年代回收的中间环节(如 CMS 的初始标记+重新标记阶段)
- 空闲列表管理本身也有开销:分配对象时需遍历链表找合适块,小对象多时明显拖慢分配速度
复制算法:高效却受限于存活率
它靠“搬活不搬死”消灭碎片、提升效率,但代价是空间冗余和复制成本。HotSpot 新生代用 Eden+Survivor(8:1:1)已大幅优化,可仍依赖一个关键前提:绝大多数对象熬不过第一次 Minor GC。
- 一旦 Survivor 区放不下存活对象(比如一次批量加载大量短生命周期缓存),就会发生“担保失败”,直接晋升到老年代——这会加速老年代填满,诱发更昂贵的 Full GC
- 若业务中存在大量中等寿命对象(存活几轮但又未达晋升阈值),Survivor 区反复复制反而增加 CPU 和内存带宽压力
- 复制过程需更新所有对象引用,GC 线程必须暂停应用线程(STW),暂停时间与存活对象数量正相关
标记-整理:平衡之选,但移动成本真实存在
它试图兼顾空间利用率和碎片控制,把存活对象往一端挤压再清理边界外内存。可“移动”不是免费的:不仅要复制对象数据,还要修正所有指向它的引用(包括栈帧、静态变量、其他对象字段等)。
- 老年代对象引用关系复杂,跨代引用(如老年代对象持有了新生代对象)需额外扫描记忆集(Remember Set),增加标记阶段开销
- 整理过程本质是顺序写 + 多次指针更新,对现代 NUMA 架构或大内存机器,可能引发非本地内存访问,进一步拉长 STW
- 某些回收器(如 Serial Old)采用单线程整理,在大堆上耗时显著,不适合低延迟场景
分代设计本身带来的隐性负担
分代是 JVM 的核心优化,但它引入了新的协调成本:
- 跨代引用检查:新生代 GC 必须确认老年代有没有引用新生代对象,否则会漏标。为此需维护卡表(Card Table)或记忆集,写屏障(Write Barrier)带来运行时性能损耗
- 代际晋升策略僵化:对象年龄达阈值(默认 15)才进老年代,但实际业务中可能存在“早熟”对象(刚创建就被长期持有)或“晚衰”对象(长期存活却总被误判为短期),导致老年代过早膨胀或 Survivor 区无谓竞争
- 各代回收节奏不同步:Minor GC 频繁,Full GC 突发,监控指标(如 GC 次数、耗时、晋升量)需关联分析,单看某一项容易误判

















