mark-compact算法压缩的是堆内存中存活对象的物理布局,通过将存活对象紧凑移动到低地址侧来消除碎片,但需更新所有引用以避免访问错乱。

Mark-Compact 算法到底在压缩什么
它不是压缩数据内容,也不是对内存做类似 ZIP 的编码;而是把堆里所有存活对象“物理搬动”,紧凑排列到内存低地址一侧,腾出一大块连续空闲空间。关键在于:搬动后必须更新所有指向这些对象的引用——否则程序会访问错乱地址。
- 触发场景:老年代 GC(Major GC)或 Full GC 中常见,尤其 CMS 已被弃用、ZGC/Shenandoah 不默认启用时,
SerialOld、ParallelOld和G1的混合回收阶段都可能走标记-压缩路径 - 代价明确:移动对象 + 修正引用 = CPU 时间上升,STW 时间通常比标记-清除长,但换来的是零碎片——避免因“有足够总内存却无连续空间”导致的频繁 GC 或 OOM
- 典型误判:“G1 是标记-复制”——不准确。G1 在回收单个 Region 时倾向复制(尤其是年轻代),但在老年代跨 Region 回收压力大、可用 Region 不足时,会退化为标记-压缩策略,即
Full GC with Mark-Compact
不同收集器里,压缩行为怎么开关和调优
没有统一开关,是否压缩、何时压缩、压缩多大范围,取决于收集器设计目标与当前堆状态。用户能干预的,主要是“让它少压缩”或“让它别退化成压缩”。
-
ParallelOld:默认全程使用标记-压缩,不可禁用;可通过-XX:UseParallelOldGC显式启用,但无法跳过压缩步骤 -
G1:正常情况按 Region 复制回收;当出现to-space exhausted(幸存区不够放存活对象)或evacuation failure,就会触发后备 Full GC,并启用标记-压缩;可加-XX:+UnlockExperimentalVMOptions -XX:G1MaxNewSizePercent=60缓解年轻代膨胀挤压老年代空间,降低退化概率 -
ZGC和Shenandoah:不压缩堆,而是靠读屏障+并发转移实现几乎无停顿;但需注意它们仍会重定位对象(本质是“带并发能力的压缩”),只是不阻塞应用线程
为什么有时候明明内存够,却还是触发了压缩型 Full GC
根本原因不是总内存不足,而是“连续空闲内存不足”。比如老年代剩余 2GB,但最大连续块只有 5MB,而新晋升对象要 10MB —— JVM 只能强制压缩腾空间。
- 常见诱因:
SurvivorRatio设置过小(如-XX:SurvivorRatio=2),导致 Survivor 空间太小,本该在年轻代就回收的对象提前进入老年代;或大量短生命周期大对象(如 byte[] 缓冲区)直接分配到老年代(通过-XX:PretenureSizeThreshold配置不当) - 诊断命令:
jstat -gc <pid></pid>查看OU(老年代已用)和OC(老年代容量)接近但OGCMN/OGCMX没变,同时FGCT突增,大概率是碎片引发的压缩型 Full GC - 一个隐蔽坑:
-XX:+UseCompressedOops在堆 >32GB 时自动失效,指针从 4B 变 8B,对象头+引用体积增大,间接加剧碎片——此时即使总内存充足,也更容易触发压缩
实操中真正该盯住的两个指标
别只看 “GC 次数” 或 “耗时”,压缩类 GC 的破坏性藏在空间利用率模式里。
立即学习“Java免费学习笔记(深入)”;
- 关注
jstat输出中的OU/OC比值持续高于 85%,且每次 GC 后下降幅度很小(比如只从 92% → 89%),说明老年代长期高水位运行,压缩风险极高 - 用
jmap -histo <pid> | head -20</pid>定期检查是否有异常多的[B(byte[])、char[]或自定义缓存类实例,它们往往是碎片制造者;若发现某类实例数量稳定但大小集中在 1–4MB 区间,大概率正在反复晋升又未及时回收
压缩本身不是 bug,是 JVM 在碎片和停顿之间做的权衡。真正要解决的,是让对象别那么早、那么大批量地进老年代——这比调任何“压缩参数”都管用。










