标记-整理算法通过标记存活对象、向一端紧凑移动、更新引用三步,为大对象整理出连续空闲空间,避免因碎片导致分配失败或Full GC;G1等收集器采用区域级部分整理以平衡停顿与效率。
标记-整理算法如何应对老年代的大对象内存碎片老年代中长期存活的对象(尤其是大数组、大集合等)反复晋升或直接分配,容易造成内存不连续——即“碎片化”。当新来的大对象需要连续空间,而老年代只剩多个小块空闲内存时,即使总空闲量足够,也会因无法满足连续性要求而触发 Full GC,甚至抛出OutOfMemoryError: Java heap space。标记-整理(Mark-Compact)正是为解决这一问题设计的:它不只回收垃圾,还主动“腾挪”存活对象,把它们向一端靠拢,从而在另一端整理出大块连续空闲空间。
压缩过程分三步:标记 → 整理 → 更新引用以 CMS 后继者 G1 或 Serial Old、Parallel Old 等使用标记-整理的收集器为例:标记阶段:从 GC Roots 出发,遍历所有可达对象并打上“存活”标记。大对象与其他对象一样被识别,不会被跳过或特殊对待。
整理阶段:按内存地址顺序,将所有已标记的存活对象(包括大对象本身及其内部引用的其他对象)向起始端(如低地址)紧凑移动。移动过程中,大对象占据的整块连续区域会被整体平移,不拆分、不压缩内容,只调整起始位置。
更新引用:对象移动后,所有指向它的引用(栈中局部变量、其他对象字段、常量池等)必须同步更新为新地址。JVM 在整理时会批量修正,确保后续访问准确无误。
对大对象变量的实际优化效果标记-整理对大对象变量的优化不是“减小其体积”,而是提升其分配成功率和长期稳定性:避免因碎片导致的大对象分配失败:整理后产生的大块空闲区,可直接容纳新创建的 4MB 数组或百万级 ArrayList,无需降级到 Full GC。
降低老年代 GC 频率:碎片减少后,老年代可用空间利用率提高,晋升对象更易容纳,延缓达到 GC 触发阈值的时间。
缓解“踩踏效应”:若不整理,多个大对象分散分布,每次分配都需搜索合适空隙;整理后空间规整,分配器(如空闲链表)能快速定位,提升吞吐。
注意局限:整理本身有开销(移动+更新),尤其在大对象占比高时,暂停时间(Stop-The-World)可能延长。因此 G1 改用“部分整理”(Region 级复制),而 ZGC/Shenandoah 则通过读屏障实现并发整理,几乎消除停顿。
配合 JVM 参数强化整理效果仅靠算法不够,需结合参数让整理更及时、更有效:-XX:+UseG1GC:启用 G1,它按 Region 划分老年代,每次回收选若干 Region 整理,兼顾效率与停顿控制。
-XX:MaxGCPauseMillis=200:指导 G1 尽量在 200ms 内完成一次 GC,促使它更积极地选择含大对象的 Region 进行整理。
-XX:PretenureSizeThreshold=1M:让 ≥1MB 的对象直接进老年代,避开年轻代复制,减少老年代中因晋升带来的细碎对象,从源头减轻整理压力。
-XX:+AlwaysPreTouch:启动时预触内存页,使老年代物理内存连续性更好,间接提升整理后空间的可用质量。
