Java 中的垃圾回收算法

Java 中的垃圾回收算法

本文内容由 AI 辅助生成,已经人工审核和编辑。

一、核心垃圾回收算法

1. 标记 - 清除算法(Mark-Sweep)

核心思想:分为两个阶段 ——

  • 标记阶段:遍历所有可达对象(从 GC Roots 出发能找到的对象),标记为 “存活”;

  • 清除阶段:遍历堆内存,将未标记的对象(垃圾)回收,释放内存。

通俗比喻:整理房间时,先把有用的物品贴上标签(标记),再把没贴标签的杂物扔掉(清除)。

优缺点

  • ✅ 实现简单,无需移动对象;

  • ❌ 会产生大量内存碎片(零散的空闲内存块),导致后续大对象无法分配;

  • ❌ 效率低(两次全堆遍历)。

适用场景:极少单独使用,仅作为基础算法。

2. 标记 - 复制算法(Mark-Copy)

核心思想:将堆内存分为大小相等的两块(From 区、To 区),只使用其中一块(From 区);

  • 标记阶段:标记 From 区的存活对象;

  • 复制阶段:将存活对象复制到 To 区,按顺序排列;

  • 交换阶段:清空 From 区,交换 From/To 区的角色。

通俗比喻:把房间分成 A、B 两半,只用 A 半放东西;整理时把 A 半的有用物品搬到 B 半并摆整齐,然后清空 A 半,下次用 A 半装新东西。

优缺点

  • ✅ 无内存碎片,分配内存时只需指针移动;

  • ✅ 效率高(仅复制存活对象);

  • ❌ 内存利用率低(仅 50%);

  • ❌ 存活对象多的时候复制成本高。

适用场景:Java 新生代(Young Generation)的默认算法(如 Serial/ParNew GC),因为新生代对象 “朝生夕死”,存活对象少,复制成本低。

代码级理解(模拟逻辑)

// 模拟标记-复制算法的核心逻辑
public class MarkCopySimulator {
    // 模拟堆内存的两个区域
    private Object[] fromSpace = new Object[10];
    private Object[] toSpace = new Object[10];

    public void gc() {
        int toIndex = 0;
        // 1. 标记并复制存活对象到To区
        for (int i = 0; i < fromSpace.length; i++) {
            Object obj = fromSpace[i];
            // 模拟:判断对象是否存活(从GC Roots可达)
            if (isAlive(obj)) {
                toSpace[toIndex++] = obj; // 复制到To区,按顺序排列
                fromSpace[i] = null; // 清空原位置
            }
        }
        // 2. 交换From/To区(简化版)
        Object[] temp = fromSpace;
        fromSpace = toSpace;
        toSpace = temp;
        // 清空To区
        Arrays.fill(toSpace, null);
    }

    // 模拟判断对象是否存活
    private boolean isAlive(Object obj) {
        return obj != null; // 简化逻辑:非null即存活
    }
}

3. 标记 - 整理算法(Mark-Compact)

核心思想:在标记 - 清除的基础上增加 “整理” 阶段 ——

  • 标记阶段:和标记 - 清除一致;

  • 整理阶段:将所有存活对象向内存一端移动,然后直接清理边界外的所有内存。

通俗比喻:整理房间时,先标记有用物品,再把所有有用物品挪到房间一侧摆整齐,最后把另一侧的所有杂物一次性扔掉。

优缺点

  • ✅ 无内存碎片,内存利用率 100%;

  • ❌ 整理阶段需要移动对象,耗时较长。

适用场景:Java 老年代(Old Generation),因为老年代对象存活率高,整理的收益大于成本。

4. 分代收集算法(Generational Collection)

核心思想:不是独立算法,而是基于 “对象存活周期不同” 的分代策略 ——

  • 将堆分为新生代(Young)和老年代(Old):

    • 新生代:对象创建和销毁频繁,用标记 - 复制算法(效率高);

    • 老年代:对象存活时间长,用标记 - 整理算法(无碎片)。

  • 新生代又细分为 Eden 区、Survivor 0 区、Survivor 1 区(比例通常 8:1:1),进一步优化复制效率。

通俗比喻:家里的垃圾桶(新生代)每天倒,用 “快速扔掉 + 重新摆” 的方式;储物间(老年代)半年整理一次,用 “归位 + 整体清理” 的方式。

核心流程

  1. 新对象优先分配到 Eden 区;

  2. Eden 区满时触发 Minor GC,存活对象复制到 Survivor 0 区;

  3. 下次 Minor GC 时,Eden + Survivor 0 的存活对象复制到 Survivor 1 区,清空 Eden 和 Survivor 0;

  4. 存活对象在 Survivor 区多次(默认 15 次)GC 后仍存活,进入老年代;

  5. 老年代满时触发 Full GC,用标记 - 整理算法回收,耗时远大于 Minor GC。

5. 分区收集算法(Region-Based Collection)

核心思想:将堆内存划分为多个大小相等的 “区域(Region)”,每次只回收部分区域,而非全堆扫描。

  • 每个 Region 可独立进行垃圾回收,减少单次 GC 的停顿时间。

适用场景:G1 GC(Garbage-First)、ZGC、Shenandoah GC 等现代垃圾收集器,适合大内存(如 8G 以上)应用。

二、各算法对比表

算法

核心特点

优点

缺点

适用区域

标记 - 清除

标记 + 直接清除垃圾

实现简单、不移动对象

内存碎片、效率低

几乎不单独用

标记 - 复制

标记 + 复制存活对象到新区域

无碎片、效率高

内存利用率低(50%)

新生代

标记 - 整理

标记 + 整理 + 清除

无碎片、利用率 100%

移动对象、耗时久

老年代

分代收集

分新生代 / 老年代用不同算法

兼顾效率和利用率

实现复杂

整体堆内存

分区收集

按区域回收

降低单次 GC 停顿时间

实现复杂

大内存应用

总结

  1. 基础算法:标记 - 清除(简单但有碎片)、标记 - 复制(高效无碎片但浪费内存)、标记 - 整理(无碎片但耗时)是三大核心;

  2. 实战策略:分代收集是 Java 最基础的 GC 策略,新生代用标记 - 复制,老年代用标记 - 整理;

  3. 现代优化:分区收集(如 G1)是为了解决大内存场景下的 GC 停顿问题,是分代收集的进阶版。

这些算法的核心目标都是:高效识别并回收垃圾,同时最小化内存碎片和 GC 停顿时间

为什么 MySQL 选择使用 B+ 树作为索引结构? 2026-02-10
Redis 雪崩场景重现与解决方案 2026-03-04

评论区