JVM 垃圾回收算法详解

垃圾回收是 JVM 最重要的机制之一,也是面试高频考点

🎯 面试重点

📖 垃圾回收概述

什么是垃圾?

/**
 * 垃圾:无用的对象占据着内存空间
 * 判断对象是否存活的算法:
 * 1. 引用计数算法
 * 2. 可达性分析算法
 */
public class GCHellowWorld {
    public static void main(String[] args) {
        // 循环引用示例
        Object a = new Object();
        Object b = new Object();
        a = null;  // a 变成垃圾
        b = null;  // b 变成垃圾
        // 此时两个对象互相引用,但都没有外部引用,仍然是垃圾
    }
}

引用计数算法

/**
 * 原理:对象每被引用一次,计数器+1;引用失效时-1
 * 缺点:无法处理循环引用
 */
public class ReferenceCounting {
    public Object instance;
    public static void main(String[] args) {
        // 循环引用问题
        ReferenceCounting objA = new ReferenceCounting();
        ReferenceCounting objB = new ReferenceCounting();
        
        objA.instance = objB;
        objB.instance = objA;
        
        objA = null;
        objB = null;
        
        // 此时两个对象引用计数都不为0,但实际上已经不可达
        // 纯引用计数算法无法识别这种垃圾
    }
}

可达性分析算法(主流)

/**
 * 原理:从 GC Root 出发,可达的对象存活,不可达的对象是垃圾
 *
 * GC Root 包含:
 * - 虚拟机栈中引用的对象
 * - 方法区中静态属性引用的对象
 * - 方法区中常量引用的对象
 * - 本地方法栈中 Native 方法引用的对象
 */
public class GCReachability {
    // 静态属性 - GC Root
    private static Object staticObject;
    
    // 局部变量 - GC Root
    private Object instance;
    
    public static void main(String[] args) {
        // 可达
        staticObject = new Object();
        
        // 不可达(局部变量随着方法结束出栈)
        Object local = new Object();
        local = null;
    }
}

📖 垃圾回收算法

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

/**
 * 过程:
 * 1. 标记阶段:标记所有需要回收的对象
 * 2. 清除阶段:统一清除被标记的对象
 *
 * 缺点:
 * - 效率低(标记和清除都慢)
 * - 空间碎片化(产生不连续的内存)
 */
public class MarkSweepDemo {
    /*
     * 标记前:
     * [对象A][空闲][对象B][对象C][空闲]
     *
     * 标记后(假设B是垃圾):
     * [对象A][空闲][XXXXX][对象C][空闲]
     *
     * 清除后:
     * [对象A][空闲        ][对象C][空闲]
     *       ↑ 产生内存碎片
     */
}

图示:

标记阶段                    清除阶段
┌───┬───┬───┬───┐        ┌───┬───┬───┬───┐
│ A │ B │ C │   │   →    │ A │   │ C │   │
└───┴───┴───┴───┘        └───┴───┴───┴───┘
   ✓      ✗   ✓            ✓      ✗   ✓

结果:产生内存碎片

2. 复制算法(Copying)

/**
 * 原理:将内存分为两半,每次只使用一半
 * 存活对象复制到另一半,然后清除原半区
 *
 * 优点:无碎片、简单高效
 * 缺点:可用内存减半
 *
 * 商用实现:Eden + Survivor(8:1:1)
 */
public class CopyingDemo {
    /*
     * 初始状态:
     * ┌─────────────────────────┬─────────────────────────┐
     │        From 区           │        To 区            │
     │  [A][B][C][D][存活对象]   │        [空]            │
     │  (需要回收)              │                         │
     * └─────────────────────────┴─────────────────────────┘
     *
     * 复制后:
     * ┌─────────────────────────┬─────────────────────────┐
     │        From 区           │        To 区            │
     │        [空]              │  [A'][B'][存活对象']    │
     * └─────────────────────────┴─────────────────────────┘
     */
}

图示:

复制算法工作流程
┌─────────────────┐         ┌─────────────────┐
│     内存块A     │         │     内存块B     │
│ [A][B][C][存活] │  复制   │ [A'][B'][C']   │
└────────┬────────┘  ──→   └─────────────────┘
         │ 清除
         ↓
┌─────────────────┐
│     内存块A     │
│    [全部清空]   │
└─────────────────┘

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

/**
 * 原理:标记后移动存活对象到一端,然后清理边界外的内存
 *
 * 优点:无碎片
 * 缺点:移动对象开销大(需要更新引用)
 *
 * 适用:老年代(对象存活率高)
 */
public class MarkCompactDemo {
    /*
     * 标记后:
     * [A][B][C][空闲][D][E][空闲]
     *      ↑
     *   存活对象
     *
     * 整理后:
     * [A][B][C][D][E][空闲       ]
     *                ↑
     *             整理边界
     */
}

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

/**
 * 核心思想:不同对象生命周期不同,采用不同的收集策略
 *
 * ┌─────────────────────────────────────────┐
 │                  堆内存                    │
 │  ┌─────────────┬───────────────────────┐│
 │  │   新生代    │       老年代           ││
 │  │ (Minor GC) │    (Major/Full GC)     ││
 │  │  复制算法   │   标记-整理算法        ││
 │  └─────────────┴───────────────────────┘│
 │ 新生代 90% 对象朝生夕死                   │
 │ 老年代 多次 GC 后仍存活                   │
 └─────────────────────────────────────────┘
 */
public class GenerationalDemo {
    public static void main(String[] args) {
        // 新生代对象:短生命周期
        for (int i = 0; i < 100; i++) {
            String s = "temp" + i;  // 使用完立即成为垃圾
        }
        
        // 老年代对象:长生命周期
        List<String> cache = new ArrayList<>();
        for (int i = 0; i < 1000; i++) {
            cache.add("persistent" + i);  // 长期存活
        }
    }
}

📖 垃圾收集器

新生代收集器

/**
 * Serial 收集器(单线程)
 * - 复制算法
 * - Stop The World
 * - 简单高效(无线程开销)
 * - 客户端模式
 */
public class SerialGC {
    // -XX:+UseSerialGC
}

/**
 * ParNew 收集器(多线程)
 * - 复制算法
 * - Stop The World
 * - 可与 CMS 配合
 * - 服务端模式
 */
public class ParNewGC {
    // -XX:+UseParNewGC
}

/**
 * Parallel Scavenge 收集器
 * - 复制算法
 * - 吞吐量优先
 * - 自适应调节
 */
public class ParallelScavengeGC {
    // -XX:+UseParallelGC
    // -XX:MaxGCPauseMillis=100  // 最大停顿时间
    // -XX:GCTimeRatio=19        // 吞吐量
}

老年代收集器

/**
 * Serial Old 收集器
 * - 标记-整理算法
 * - 单线程
 */
public class SerialOldGC {
    // -XX:+UseSerialOldGC
}

/**
 * Parallel Old 收集器
 * - 标记-整理算法
 * - 多线程
 */
public class ParallelOldGC {
    // -XX:+UseParallelOldGC
}

/**
 * CMS 收集器(Concurrent Mark Sweep)
 * - 标记-清除算法
 * - 并发收集、低停顿
 * - 浮动垃圾问题
 * - 内存碎片问题
 */
public class CMSGC {
    // -XX:+UseConcMarkSweepGC
    /*
     * 收集过程:
     * 1. 初始标记(STW)     - 标记 GC Root
     * 2. 并发标记            - 追踪存活对象
     * 3. 重新标记(STW)     - 修正变化
     * 4. 并发清除            - 清除垃圾
     */
}

/**
 * G1 收集器(Garbage First)
 * - 复制+标记-整理
 * - 区域(Region)概念
 * - 可预测停顿
 * - JDK 9+ 默认
 */
public class G1GC {
    // -XX:+UseG1GC
    // -XX:MaxGCPauseMillis=200
}

各收集器对比

收集器 区域 算法 特点 适用场景
Serial 新生代 复制 单线程、Client 小型应用
ParNew 新生代 复制 多线程、Server 多核 + CMS
Parallel 新生代 复制 吞吐量优先 后台计算
Serial Old 老年代 整理 单线程 小型应用
Parallel Old 老年代 整理 多线程 后台计算
CMS 老年代 清除 并发、低停顿 互联网
G1 全代 整理+复制 可预测停顿 JDK 9+

📖 面试真题

Q1: 为什么分代回收?

答: 根据对象生命周期特点,90% 的对象是短命鬼(朝生夕死),只有少数对象存活时间长。新生代用复制算法(高效处理死亡对象),老年代用标记-整理(适合存活对象多的情况)。

Q2: G1 有什么特点?

答:

  1. 区域(Region)概念:将堆划分为多个大小相等的区域
  2. 卡片(Card):每个 Region 进一步划分,用于并发标记
  3. 可预测停顿:支持设置最大停顿时间
  4. 软实时:优先回收垃圾最多的区域
  5. 并发标记:减少 STW 时间
  6. JDK 9+ 成为默认收集器

Q3: CMS 有什么问题?

答:

  1. 浮动垃圾:并发清理阶段产生的垃圾无法本次清理
  2. 内存碎片:标记-清除算法导致碎片,需要 Full GC 整理
  3. 并发失败:预留内存不够时退化为 Serial Old

Q4: 对象分配过程?

答:

  1. 优先在 Eden 区分配
  2. 大对象(超过阈值)直接进入老年代
  3. 长期存活对象进入 Survivor 区
  4. 对象 age 超过阈值(默认 15)进入老年代

Q5: 垃圾回收算法有哪些?各有什么优缺点?

答: 垃圾回收算法主要有以下几种:

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

2. 复制算法(Copying)

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

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

总结:不同算法适用于不同场景,选择合适算法需要在空间、时间和实现复杂度之间权衡。


⭐ 重点:垃圾回收算法是 JVM 核心,面试必问,务必掌握各算法优缺点和适用场景