欢迎光临
我们一直在努力

Java垃圾回收GC核心原理:标记清除、复制、标记整理算法详解【万字干货+代码实战+调优解法】

Java垃圾回收GC核心原理:标记清除、复制、标记整理算法详解【万字干货+代码实战+调优解法】

**标签:**Java GC、JVM调优、垃圾回收算法、标记清除、复制算法、标记整理、内存碎片、STW

**简介:**本文深度剖析Java三大核心垃圾回收算法,从底层原理、执行流程、源码模拟、优缺点、适用场景全方位拆解,附带大量可运行Java实战代码、问题解法、性能优化方案,同时对比各算法落地差异,解决开发中GC卡顿、内存溢出、碎片过多等核心问题,适合JVM进阶、性能调优学习与面试复盘。

一、GC前置核心基础(必看)

1.1 什么是垃圾回收GC

GC(Garbage Collection,垃圾回收)是Java虚拟机自动内存管理机制,无需开发者手动申请、释放堆内存,JVM会自动识别内存中无效垃圾对象,回收其占用内存,避免内存泄漏、内存溢出问题。

核心目标:自动回收无效对象内存、减少内存碎片、降低STW停顿时间、提升内存利用率。

1.2 垃圾判定核心:可达性分析算法

Java不采用传统引用计数法(无法解决循环引用垃圾),而是通过GC Roots可达性分析判定对象是否存活:

从GC Roots(根对象)出发,遍历引用链,能遍历到的对象为存活对象,无法遍历到的为垃圾对象,等待GC回收。

常见GC Roots对象:

  • 虚拟机栈中局部变量引用的对象

  • 方法区静态变量、常量引用的对象

  • 本地方法栈JNI引用的对象

  • 活跃线程、锁对象等

1.3 GC核心术语铺垫

  • STW(Stop-The-World):GC执行时暂停所有用户业务线程,仅保留GC线程工作,是GC卡顿的核心原因

  • 内存碎片:GC回收后产生大量不连续小内存块,导致大对象无法分配

  • 分代回收:JVM将堆内存分为新生代、老年代,不同区域采用不同GC算法,提升回收效率

  • 对象晋升:新生代存活对象经过多次GC后,晋升至老年代

二、三大核心GC算法深度详解(原理+代码+优缺点)

Java所有垃圾回收器(CMS、G1、ZGC、Parallel GC)底层均基于三大基础算法衍生优化,分别是:标记清除算法、复制算法、标记整理算法。

2.1 标记清除算法(Mark-Sweep)——最基础GC算法

2.1.1 核心执行原理

算法分为两个核心阶段,全程伴随STW:

  • 标记阶段:从GC Roots遍历所有对象,标记所有可达的存活对象

  • 清除阶段:遍历整个堆内存,回收所有未被标记的垃圾对象,释放对应内存

  • 核心逻辑:只标记存活、只清理垃圾,不移动任何对象位置。

    2.1.2 算法执行流程图解

    初始内存:内存中存在存活对象A、B,垃圾对象C、D、E

    标记后:A、B标记为存活,C、D、E无标记

    清除后:C、D、E内存释放,产生大量不连续空闲内存碎片

    2.1.3 Java代码模拟标记清除算法

    通过自定义内存块、对象引用、标记逻辑,完整模拟标记清除执行流程:

    /**
    * 标记清除算法 代码模拟
    * 核心流程:标记存活对象 -> 清除未标记垃圾对象
    */

    public class MarkSweepGC {
    // 模拟堆内存最大容量
    private static final int MAX_MEMORY = 100;
    // 模拟内存中的对象数组
    private static GCObject[] heapMemory = new GCObject[MAX_MEMORY];

    // 自定义内存对象
    static class GCObject {
    // 对象是否被标记(true=存活,false=垃圾)
    boolean isMarked;
    // 模拟对象引用
    GCObject reference;
    // 对象占用内存大小
    int size;

    public GCObject(int size) {
    this.size = size;
    this.isMarked = false;
    }
    }

    // 1. 标记阶段:从GC Roots标记所有存活对象
    public static void markPhase(GCObject root) {
    // 递归标记可达对象
    if (root != null && !root.isMarked) {
    root.isMarked = true;
    // 标记当前对象引用的下级对象
    markPhase(root.reference);
    }
    }

    // 2. 清除阶段:回收所有未标记的垃圾对象
    public static int sweepPhase() {
    int freeMemory = 0;
    for (int i = 0; i < heapMemory.length; i++) {
    GCObject obj = heapMemory[i];
    // 未标记的对象为垃圾,回收内存
    if (obj != null && !obj.isMarked) {
    freeMemory += obj.size;
    heapMemory[i] = null; // 清空对象,释放内存
    }
    }
    System.out.println("【标记清除】本次回收空闲内存:" + freeMemory);
    return freeMemory;
    }

    // 执行完整标记清除GC
    public static void markSweepGC(GCObject gcRoot) {
    // STW开始
    System.out.println("开始执行标记清除GC,STW停顿中…");
    // 阶段1:标记存活对象
    markPhase(gcRoot);
    // 阶段2:清除垃圾对象
    sweepPhase();
    // STW结束
    System.out.println("标记清除GC执行完毕,内存存在碎片\\n");
    }

    public static void main(String[] args) {
    // 模拟创建存活对象(GC Roots可达)
    GCObject liveObj1 = new GCObject(20);
    GCObject liveObj2 = new GCObject(15);
    liveObj1.reference = liveObj2;

    // 模拟创建垃圾对象(无GC Roots引用)
    GCObject garbage1 = new GCObject(10);
    GCObject garbage2 = new GCObject(25);

    // 存入堆内存
    heapMemory[0] = liveObj1;
    heapMemory[1] = liveObj1.reference;
    heapMemory[2] = garbage1;
    heapMemory[3] = garbage2;

    // 执行GC,以liveObj1作为GC Roots
    markSweepGC(liveObj1);
    }
    }

    2.1.4 算法优缺点深度分析

    优点:

  • 算法逻辑简单、实现难度低

  • 无需移动对象、无需修改引用地址,执行成本低

  • 不占用额外内存空间,无内存冗余

  • 缺点(核心痛点):

  • 产生大量内存碎片:回收后空闲内存不连续,大对象无法分配,容易触发Full GC

  • 两次遍历堆内存:标记、清除各遍历一次,堆越大效率越低

  • STW时间不稳定,碎片化越严重,后续GC效率越差

  • 2.1.5 适用场景与问题解法

    适用场景:老年代瞬时GC、存活对象多、垃圾对象少的场景(早期CMS回收老年代核心算法)

    常见问题&最优解法:

    • 问题1:内存碎片过多,大对象分配失败OOM
      解法:配合标记整理算法使用、开启JVM碎片整理参数、定期Full GC整理内存

    • 问题2:大堆内存GC遍历效率低
      解法:采用分代回收,缩小单次GC遍历内存范围

    2.2 复制算法(Copying)——新生代专属高效算法

    2.2.1 核心执行原理

    针对标记清除碎片问题优化,核心思想:空间换时间,无内存碎片。

    将可用堆内存平均划分为两块大小相等的内存区域,同一时间只使用其中一块(From区),另一块(To区)空闲备用。GC执行流程:

  • 标记From区所有存活对象

  • 将存活对象完整复制到To区,严格有序排列

  • 清空From区所有内存,互换From/To区域身份

  • 2.2.2 适配场景原理

    Java新生代对象朝生夕灭,90%以上对象创建后快速死亡,每次GC仅需复制少量存活对象,效率极高,完美适配新生代Minor GC。

    2.2.3 Java代码模拟复制算法

    /**
    * 复制算法 代码模拟(适配新生代Minor GC)
    * 核心流程:划分对等内存区 -> 复制存活对象 -> 清空原内存 -> 互换区域
    */

    public class CopyingGC {
    // 单个内存区域容量
    private static final int AREA_SIZE = 50;
    // From区:当前使用的内存区
    private static GCObject[] fromArea = new GCObject[AREA_SIZE];
    // To区:空闲备用内存区
    private static GCObject[] toArea = new GCObject[AREA_SIZE];

    // 复用自定义内存对象
    static class GCObject {
    boolean isMarked;
    GCObject reference;
    int size;

    public GCObject(int size) {
    this.size = size;
    this.isMarked = false;
    }
    }

    // 标记存活对象
    public static void markLiveObject(GCObject obj) {
    if (obj != null && !obj.isMarked) {
    obj.isMarked = true;
    markLiveObject(obj.reference);
    }
    }

    // 核心:复制存活对象到To区
    public static void copyLiveObject() {
    int toIndex = 0;
    // 遍历From区,复制标记的存活对象
    for (GCObject obj : fromArea) {
    if (obj != null && obj.isMarked) {
    toArea[toIndex++] = obj;
    System.out.println("复制存活对象,占用内存:" + obj.size);
    }
    }
    }

    // 清空From区,互换内存区域
    public static void swapAndClear() {
    // 清空原From区
    fromArea = new GCObject[AREA_SIZE];
    // 互换区域:原To区变为新From区,原From区变为新To区
    GCObject[] temp = fromArea;
    fromArea = toArea;
    toArea = temp;
    System.out.println("内存区域互换完成,无内存碎片");
    }

    // 执行复制GC
    public static void copyingGC(GCObject root) {
    System.out.println("开始执行复制GC,STW停顿中…");
    // 1. 标记存活对象
    markLiveObject(root);
    // 2. 复制存活对象到备用区
    copyLiveObject();
    // 3. 清空原内存,互换区域
    swapAndClear();
    System.out.println("复制GC执行完毕\\n");
    }

    public static void main(String[] args) {
    // 初始化From区:2个存活对象,多个垃圾对象
    GCObject live1 = new GCObject(10);
    GCObject live2 = new GCObject(15);
    live1.reference = live2;

    fromArea[0] = live1;
    fromArea[1] = live1.reference;
    fromArea[2] = new GCObject(8); // 垃圾对象
    fromArea[3] = new GCObject(12); // 垃圾对象

    // 执行复制GC
    copyingGC(live1);
    }
    }

    2.2.4 新生代优化:Survivor双区机制

    JVM并未直接使用1:1对等分区,而是优化为Eden:From:To = 8:1:1的内存布局,解决1:1分区内存浪费问题:

    • Eden区:新生代主要内存区域,存放新建对象

    • 两个Survivor区(From/To):对等备用区,保证复制算法执行

    • 仅浪费10%内存,内存利用率大幅提升

    2.2.5 算法优缺点与问题解法

    优点:

  • 无任何内存碎片,内存空间连续,完美支持大对象分配

  • 仅复制存活对象,垃圾对象直接清空,回收效率极高

  • STW时间短,适合高频次的新生代Minor GC

  • 缺点:

  • 内存利用率低:1:1分区浪费50%内存,优化后仍浪费10%

  • 存活对象多时代价极高:对象存活率高时,复制成本指数级上升

  • 不适合老年代大对象、长生命周期对象回收

  • 常见问题&最优解法:

    • 问题1:大对象频繁复制导致GC卡顿
      解法:大对象直接进入老年代,规避新生代复制开销;调整-XX:PretenureSizeThreshold参数

    • 问题2:对象过早晋升老年代,导致老年代膨胀
      解法:调整晋升阈值-XX:MaxTenuringThreshold,延长新生代存活时间

    2.3 标记整理算法(Mark-Compact)——老年代终极算法

    2.3.1 核心执行原理

    结合标记清除、复制算法双重优势,解决碎片问题+内存浪费问题,是老年代主流GC算法。执行分为两个阶段:

  • 标记阶段:和标记清除一致,从GC Roots标记所有存活对象

  • 整理阶段:不复制、不直接清除,而是将所有存活对象向内存一端紧凑移动,所有存活对象连续排列,最后一次性清空末端所有垃圾内存

  • 核心逻辑:标记存活、紧凑整理、批量回收。

    2.3.2 算法执行流程

    初始内存:存活对象分散、夹杂大量垃圾对象、内存碎片严重

    标记后:筛选出所有存活对象

    整理后:存活对象全部紧凑至内存前端,后端为整块连续空闲内存,无任何碎片

    2.3.3 Java代码模拟标记整理算法

    /**
    * 标记整理算法 代码模拟(老年代专属)
    * 核心流程:标记存活对象 -> 紧凑整理内存 -> 批量回收末端垃圾
    */

    public class MarkCompactGC {
    // 老年代堆内存容量
    private static final int OLD_MEMORY_SIZE = 100;
    private static GCObject[] oldMemory = new GCObject[OLD_MEMORY_SIZE];

    static class GCObject {
    boolean isMarked;
    int size;
    // 记录对象原始内存地址
    int address;

    public GCObject(int size, int address) {
    this.size = size;
    this.address = address;
    this.isMarked = false;
    }
    }

    // 1. 标记存活对象
    public static void markPhase(GCObject obj) {
    if (obj != null && !obj.isMarked) {
    obj.isMarked = true;
    }
    }

    // 2. 核心:整理内存,紧凑存活对象
    public static void compactPhase() {
    int newAddress = 0;
    // 遍历内存,将存活对象紧凑前移
    for (int i = 0; i < oldMemory.length; i++) {
    GCObject obj = oldMemory[i];
    if (obj != null && obj.isMarked) {
    // 移动存活对象到新地址
    oldMemory[newAddress++] = obj;
    System.out.println("存活对象整理:原地址" + obj.address + " -> 新地址" + newAddress);
    }
    }

    // 3. 批量清空末端垃圾内存
    for (int i = newAddress; i < oldMemory.length; i++) {
    oldMemory[i] = null;
    }
    System.out.println("内存整理完成,空闲内存连续无碎片,总空闲容量:" + (OLD_MEMORY_SIZE newAddress));
    }

    // 执行标记整理GC
    public static void markCompactGC(GCObject root) {
    System.out.println("开始执行标记整理GC,STW停顿中…");
    // 标记阶段
    markPhase(root);
    // 整理回收阶段
    compactPhase();
    System.out.println("标记整理GC执行完毕\\n");
    }

    public static void main(String[] args) {
    // 模拟老年代分散的存活对象和垃圾对象
    oldMemory[0] = new GCObject(15, 0); // 存活
    oldMemory[2] = new GCObject(10, 2); // 垃圾
    oldMemory[3] = new GCObject(20, 3); // 存活
    oldMemory[5] = new GCObject(8, 5); // 垃圾

    // 执行GC
    markCompactGC(oldMemory[0]);
    }
    }

    2.3.4 算法优缺点深度解析

    优点:

  • 无内存碎片:整理后内存完全连续,彻底解决大对象分配失败问题

  • 内存利用率100%:无需分区冗余,无内存浪费

  • 适配老年代:老年代对象存活率高、生命周期长,移动频率低

  • 缺点(核心痛点):

  • STW时间最长:需要移动大量存活对象、更新所有引用地址,开销极大

  • 执行效率低于复制、标记清除算法,不适合高频GC场景

  • 堆内存越大,整理耗时越久

  • 2.3.5 适用场景与优化解法

    适用场景:老年代Full GC、对象存活率高、对内存碎片敏感、低频GC场景(G1、ZGC老年代回收核心算法)

    常见问题&最优解法:

    • 问题1:标记整理STW过长,业务卡顿
      解法:采用并发标记整理、分块整理;使用G1/ZGC低延迟垃圾回收器

    • 问题2:频繁Full GC导致服务雪崩
      解法:优化内存参数、避免大对象频繁创建、防止内存泄漏、控制对象晋升频率

    三、三大GC算法全方位对比(面试必背)

    对比维度标记清除算法复制算法标记整理算法
    核心特点 标记存活、清理垃圾、不移动对象 分区复制存活对象、清空原内存 标记存活、紧凑整理、批量回收
    内存碎片 严重碎片 无碎片 无碎片
    内存利用率 100% 低(优化后90%) 100%
    STW停顿时间 中等 短(高频高效) 长(低频低效)
    存活率适配 高低存活率均可 仅适配低存活率 仅适配高存活率
    适用区域 老年代(临时回收) 新生代(Minor GC) 老年代(Full GC)
    执行开销 中等 低(存活少)/高(存活多) 极高

    四、JVM分代GC算法组合落地(生产环境核心)

    4.1 分代算法组合逻辑

    JVM没有单一使用某一种算法,而是分代适配、组合使用,最大化GC效率:

    • 新生代(Minor GC):核心使用复制算法
      原因:对象朝生夕灭、存活率低、需要高频快速回收、追求低STW

    • 老年代(Full GC):核心使用标记清除+标记整理组合算法
      原因:对象存活率高、生命周期长、低频GC、优先保证内存无碎片

    4.2 主流垃圾回收器底层算法对应关系

    • Serial GC:新生代复制算法、老年代标记整理

    • Parallel GC:新生代复制算法、老年代标记整理(并行优化)

    • CMS GC:老年代标记清除为主,碎片过多时触发标记整理

    • G1 GC:分区复制+标记整理混合算法

    • ZGC/Shenandoah:并发标记+移动整理,超低延迟优化

    五、生产环境GC问题实战解法(高频故障)

    5.1 问题1:新生代GC频繁、CPU飙升

    根因:新生代内存过小、短生命周期对象创建过多,复制算法频繁执行

    解决方案:

  • 调大新生代内存:-Xmn 调整新生代容量

  • 优化代码:减少临时对象、字符串、集合频繁创建

  • 调整Survivor比例,减少对象过早晋升

  • 5.2 问题2:老年代内存碎片、大对象OOM

    根因:CMS标记清除算法产生大量内存碎片,连续内存不足

    解决方案:

  • 开启CMS碎片整理:-XX:+UseCMSCompactAtFullCollection

  • 设置整理频率:-XX:CMSFullGCsBeforeCompaction=3

  • 替换G1/ZGC回收器,从底层规避碎片问题

  • 5.3 问题3:Full GC卡顿严重、业务超时

    根因:老年代标记整理算法STW时间过长,大量对象移动、引用更新

    解决方案:

  • 升级低延迟回收器:G1、ZGC、Shenandoah

  • 避免大对象直接创建,优化对象生命周期

  • 排查内存泄漏,杜绝老年代内存持续膨胀

  • 六、高频面试总结(必考问答)

  • 为什么新生代用复制算法,老年代不用?
    新生代存活率低、高频GC,复制算法效率高、无碎片;老年代存活率高,复制成本极高,适合标记整理

  • 标记清除和标记整理的核心区别?
    标记清除只清不挪,有碎片、STW短;标记整理先挪后清,无碎片、STW长

  • 复制算法为什么要分区?
    保证存活对象有序复制,清空原内存,彻底消除内存碎片

  • GC算法核心演进逻辑?
    标记清除(简单有碎片)→ 复制算法(无碎片耗内存)→ 标记整理(无碎片高稳定)

  • 七、总结

    三大GC算法是Java垃圾回收的底层基石,没有绝对最优的算法,只有场景适配的算法:

    • 追求高频快速、低延迟 → 新生代复制算法

    • 追求简单高效、中等开销 → 标记清除算法

    • 追求内存规整、无碎片、高利用率 → 老年代标记整理算法

    生产环境GC调优的核心,就是根据业务场景匹配算法、规避算法缺陷、优化参数阈值,从底层解决GC卡顿、OOM、内存碎片等核心问题。

    赞(0)
    未经允许不得转载:171主机测评 » Java垃圾回收GC核心原理:标记清除、复制、标记整理算法详解【万字干货+代码实战+调优解法】
    分享到: 更多 (0)

    评论 抢沙发

    • 昵称 (必填)
    • 邮箱 (必填)
    • 网址