标记-整理算法(Mark-Compact)是老年代垃圾回收的核心算法之一,在老年代中存在大量大对象变量时,该算法通过特定的内存整理逻辑,能够有效减少内存碎片,提升内存利用率。大对象通常指需要连续内存空间存储的对象,比如长数组、大字符串等,这类对象进入老年代后如果频繁触发垃圾回收,很容易产生大量不连续的内存碎片,导致后续大对象无法分配足够的连续空间,进而触发Full GC。标记-整理算法正是针对这类场景设计了内存压缩优化的流程。

标记-整理算法的基础执行流程
标记-整理算法的执行分为三个阶段,分别是标记阶段、整理阶段、更新引用阶段,整体流程如下:
- 标记阶段:从GC Roots出发,遍历所有存活对象,给存活对象打上标记,区分出存活对象和垃圾对象。
- 整理阶段:将所有存活对象向内存空间的一端移动,移动完成后,清理掉边界以外的所有内存空间,保证存活对象占用连续的内存区域。
- 更新引用阶段:由于存活对象的内存地址发生了改变,需要更新所有指向这些对象的引用地址,保证引用的正确性。
老年代大对象的存储特点
老年代的大对象有以下几个典型特点,这些特点也是标记-整理算法需要做针对性优化的原因:
- 大对象需要连续的存储空间,即使剩余总内存足够,如果碎片空间不连续,也无法分配大对象。
- 大对象一般不会频繁变成垃圾,生命周期较长,进入老年代后长期存活的概率更高。
- 大对象移动的成本较高,移动大对象需要复制更多的内存数据,会消耗更多的GC时间。
针对大对象的内存压缩优化逻辑
标记-整理算法在处理老年代大对象时,会通过以下优化逻辑降低移动成本,提升压缩效率:
1. 大对象优先移动策略
在整理阶段,算法会优先处理大对象,将大对象移动到内存空间的最前端连续区域,避免小对象穿插在大对象之间,减少后续小对象移动时反复调整大对象位置的情况。具体实现逻辑可以通过对象大小排序实现,示例代码如下:
import java.util.ArrayList;
import java.util.List;
// 模拟老年代对象结构
class OldGenObject {
// 对象大小,单位字节
long size;
// 对象是否存活
boolean isAlive;
// 对象当前内存地址
long currentAddress;
// 对象新的内存地址
long newAddress;
public OldGenObject(long size, boolean isAlive, long currentAddress) {
this.size = size;
this.isAlive = isAlive;
this.currentAddress = currentAddress;
}
}
public class MarkCompactOptimizer {
// 大对象阈值,超过这个值视为大对象,单位字节
private static final long LARGE_OBJECT_THRESHOLD = 1024 * 1024;
// 整理存活对象,优先移动大对象
public static void compactObjects(List<OldGenObject> objects) {
// 先筛选出所有存活对象
List<OldGenObject> aliveObjects = new ArrayList<>();
for (OldGenObject obj : objects) {
if (obj.isAlive) {
aliveObjects.add(obj);
}
}
// 按对象大小降序排序,大对象在前
aliveObjects.sort((o1, o2) -> Long.compare(o2.size, o1.size));
// 当前整理到的内存起始地址
long currentCompactAddress = 0;
for (OldGenObject obj : aliveObjects) {
// 记录对象新的地址
obj.newAddress = currentCompactAddress;
// 移动对象数据到新地址(模拟操作)
moveObject(obj.currentAddress, obj.newAddress, obj.size);
// 更新当前整理地址,加上当前对象的大小
currentCompactAddress += obj.size;
}
}
// 模拟移动对象内存数据的方法
private static void moveObject(long srcAddress, long destAddress, long size) {
// 实际JVM中会复制对应大小的内存数据
System.out.println("移动对象,大小:" + size + "字节,从" + srcAddress + "到" + destAddress);
}
}
2. 大对象引用批量更新
大对象的引用通常数量较少,算法会在更新引用阶段,先批量处理大对象的所有引用,避免多次遍历引用列表。同时对于大对象的引用更新,会直接修改引用的指针地址,不需要额外的校验逻辑,减少更新耗时。示例引用更新逻辑如下:
import java.util.HashMap;
import java.util.Map;
// 模拟引用记录结构
class ObjectReference {
// 引用的持有者对象
OldGenObject holder;
// 引用的目标对象
OldGenObject target;
public ObjectReference(OldGenObject holder, OldGenObject target) {
this.holder = holder;
this.target = target;
}
}
public class ReferenceUpdater {
// 批量更新大对象引用
public static void updateLargeObjectReferences(Map<OldGenObject, List<ObjectReference>> referenceMap) {
for (Map.Entry<OldGenObject, List<ObjectReference>> entry : referenceMap.entrySet()) {
OldGenObject targetObj = entry.getKey();
// 只处理大对象的引用更新
if (targetObj.size >= MarkCompactOptimizer.LARGE_OBJECT_THRESHOLD) {
List<ObjectReference> references = entry.getValue();
for (ObjectReference ref : references) {
// 更新引用指向新的对象地址
ref.target = targetObj;
// 实际JVM中会修改引用指针的值为targetObj.newAddress
System.out.println("更新大对象引用,新地址:" + targetObj.newAddress);
}
}
}
}
}
3. 碎片区域合并预判断
在标记阶段完成后,算法会先统计大对象周围的碎片空间大小,如果大对象前后的碎片空间总和足够容纳该大对象,就不会移动该大对象,只移动周围的小对象填充碎片,减少大对象的移动次数。这种预判断逻辑可以大幅降低大对象移动带来的性能损耗。
标记-整理算法与其他老年代回收算法对比
为了更直观体现标记-整理算法针对大对象的优化优势,我们将其和老年代常用的标记-清除算法做对比:
| 对比维度 | 标记-整理算法 | 标记-清除算法 |
|---|---|---|
| 内存碎片情况 | 无内存碎片,内存空间连续 | 会产生大量不连续内存碎片 |
| 大对象分配成功率 | 高,连续空间充足 | 低,容易出现连续空间不足 |
| 大对象移动成本 | 有优化策略,成本相对较低 | 不需要移动对象,无移动成本 |
| GC停顿时间 | 较长,需要移动对象和更新引用 | 较短,只需要标记和清除 |
适用场景说明
标记-整理算法适合老年代中大对象占比较高、内存分配频繁的场景,虽然其GC停顿时间比标记-清除算法长,但是能够有效避免内存碎片导致的大对象分配失败问题,减少Full GC的触发频率。如果老年代中大部分是小对象,且对GC停顿时间要求很高,那么标记-清除算法或者分代回收结合其他算法会更合适。
需要注意的是,不同的JVM实现会对标记-整理算法的大对象优化逻辑做进一步调整,比如HotSpot虚拟机的CMS收集器在并发模式失败后会使用标记-整理算法做内存压缩,其大对象处理逻辑会根据实际的老年代内存布局做动态优化。
Mark_Compact老年代大对象内存压缩垃圾回收修改时间:2026-07-18 02:54:40