Java垃圾回收GC核心原理:标记清除、复制、标记整理算法详解【万字干货+代码实战+调优解法】
2026/9/18 22:01:18 网站建设 项目流程

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:

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

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

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

2.1.2 算法执行流程图解

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

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

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

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

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

/** * 标记清除算法 代码模拟 * 核心流程:标记存活对象 -> 清除未标记垃圾对象 */publicclassMarkSweepGC{// 模拟堆内存最大容量privatestaticfinalintMAX_MEMORY=100;// 模拟内存中的对象数组privatestaticGCObject[]heapMemory=newGCObject[MAX_MEMORY];// 自定义内存对象staticclassGCObject{// 对象是否被标记(true=存活,false=垃圾)booleanisMarked;// 模拟对象引用GCObjectreference;// 对象占用内存大小intsize;publicGCObject(intsize){this.size=size;this.isMarked=false;}}// 1. 标记阶段:从GC Roots标记所有存活对象publicstaticvoidmarkPhase(GCObjectroot){// 递归标记可达对象if(root!=null&&!root.isMarked){root.isMarked=true;// 标记当前对象引用的下级对象markPhase(root.reference);}}// 2. 清除阶段:回收所有未标记的垃圾对象publicstaticintsweepPhase(){intfreeMemory=0;for(inti=0;i<heapMemory.length;i++){GCObjectobj=heapMemory[i];// 未标记的对象为垃圾,回收内存if(obj!=null&&!obj.isMarked){freeMemory+=obj.size;heapMemory[i]=null;// 清空对象,释放内存}}System.out.println("【标记清除】本次回收空闲内存:"+freeMemory);returnfreeMemory;}// 执行完整标记清除GCpublicstaticvoidmarkSweepGC(GCObjectgcRoot){// STW开始System.out.println("开始执行标记清除GC,STW停顿中...");// 阶段1:标记存活对象markPhase(gcRoot);// 阶段2:清除垃圾对象sweepPhase();// STW结束System.out.println("标记清除GC执行完毕,内存存在碎片\n");}publicstaticvoidmain(String[]args){// 模拟创建存活对象(GC Roots可达)GCObjectliveObj1=newGCObject(20);GCObjectliveObj2=newGCObject(15);liveObj1.reference=liveObj2;// 模拟创建垃圾对象(无GC Roots引用)GCObjectgarbage1=newGCObject(10);GCObjectgarbage2=newGCObject(25);// 存入堆内存heapMemory[0]=liveObj1;heapMemory[1]=liveObj1.reference;heapMemory[2]=garbage1;heapMemory[3]=garbage2;// 执行GC,以liveObj1作为GC RootsmarkSweepGC(liveObj1);}}

2.1.4 算法优缺点深度分析

优点:

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

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

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

缺点(核心痛点):

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

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

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

2.1.5 适用场景与问题解法

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

常见问题&最优解法:

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

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

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

2.2.1 核心执行原理

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

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

  1. 标记From区所有存活对象

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

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

2.2.2 适配场景原理

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

2.2.3 Java代码模拟复制算法

/** * 复制算法 代码模拟(适配新生代Minor GC) * 核心流程:划分对等内存区 -> 复制存活对象 -> 清空原内存 -> 互换区域 */publicclassCopyingGC{// 单个内存区域容量privatestaticfinalintAREA_SIZE=50;// From区:当前使用的内存区privatestaticGCObject[]fromArea=newGCObject[AREA_SIZE];// To区:空闲备用内存区privatestaticGCObject[]toArea=newGCObject[AREA_SIZE];// 复用自定义内存对象staticclassGCObject{booleanisMarked;GCObjectreference;intsize;publicGCObject(intsize){this.size=size;this.isMarked=false;}}// 标记存活对象publicstaticvoidmarkLiveObject(GCObjectobj){if(obj!=null&&!obj.isMarked){obj.isMarked=true;markLiveObject(obj.reference);}}// 核心:复制存活对象到To区publicstaticvoidcopyLiveObject(){inttoIndex=0;// 遍历From区,复制标记的存活对象for(GCObjectobj:fromArea){if(obj!=null&&obj.isMarked){toArea[toIndex++]=obj;System.out.println("复制存活对象,占用内存:"+obj.size);}}}// 清空From区,互换内存区域publicstaticvoidswapAndClear(){// 清空原From区fromArea=newGCObject[AREA_SIZE];// 互换区域:原To区变为新From区,原From区变为新To区GCObject[]temp=fromArea;fromArea=toArea;toArea=temp;System.out.println("内存区域互换完成,无内存碎片");}// 执行复制GCpublicstaticvoidcopyingGC(GCObjectroot){System.out.println("开始执行复制GC,STW停顿中...");// 1. 标记存活对象markLiveObject(root);// 2. 复制存活对象到备用区copyLiveObject();// 3. 清空原内存,互换区域swapAndClear();System.out.println("复制GC执行完毕\n");}publicstaticvoidmain(String[]args){// 初始化From区:2个存活对象,多个垃圾对象GCObjectlive1=newGCObject(10);GCObjectlive2=newGCObject(15);live1.reference=live2;fromArea[0]=live1;fromArea[1]=live1.reference;fromArea[2]=newGCObject(8);// 垃圾对象fromArea[3]=newGCObject(12);// 垃圾对象// 执行复制GCcopyingGC(live1);}}

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

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

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

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

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

2.2.5 算法优缺点与问题解法

优点:

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

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

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

缺点:

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

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

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

常见问题&最优解法:

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

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

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

2.3.1 核心执行原理

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

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

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

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

2.3.2 算法执行流程

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

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

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

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

/** * 标记整理算法 代码模拟(老年代专属) * 核心流程:标记存活对象 -> 紧凑整理内存 -> 批量回收末端垃圾 */publicclassMarkCompactGC{// 老年代堆内存容量privatestaticfinalintOLD_MEMORY_SIZE=100;privatestaticGCObject[]oldMemory=newGCObject[OLD_MEMORY_SIZE];staticclassGCObject{booleanisMarked;intsize;// 记录对象原始内存地址intaddress;publicGCObject(intsize,intaddress){this.size=size;this.address=address;this.isMarked=false;}}// 1. 标记存活对象publicstaticvoidmarkPhase(GCObjectobj){if(obj!=null&&!obj.isMarked){obj.isMarked=true;}}// 2. 核心:整理内存,紧凑存活对象publicstaticvoidcompactPhase(){intnewAddress=0;// 遍历内存,将存活对象紧凑前移for(inti=0;i<oldMemory.length;i++){GCObjectobj=oldMemory[i];if(obj!=null&&obj.isMarked){// 移动存活对象到新地址oldMemory[newAddress++]=obj;System.out.println("存活对象整理:原地址"+obj.address+" -> 新地址"+newAddress);}}// 3. 批量清空末端垃圾内存for(inti=newAddress;i<oldMemory.length;i++){oldMemory[i]=null;}System.out.println("内存整理完成,空闲内存连续无碎片,总空闲容量:"+(OLD_MEMORY_SIZE-newAddress));}// 执行标记整理GCpublicstaticvoidmarkCompactGC(GCObjectroot){System.out.println("开始执行标记整理GC,STW停顿中...");// 标记阶段markPhase(root);// 整理回收阶段compactPhase();System.out.println("标记整理GC执行完毕\n");}publicstaticvoidmain(String[]args){// 模拟老年代分散的存活对象和垃圾对象oldMemory[0]=newGCObject(15,0);// 存活oldMemory[2]=newGCObject(10,2);// 垃圾oldMemory[3]=newGCObject(20,3);// 存活oldMemory[5]=newGCObject(8,5);// 垃圾// 执行GCmarkCompactGC(oldMemory[0]);}}

2.3.4 算法优缺点深度解析

优点:

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

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

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

缺点(核心痛点):

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

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

  3. 堆内存越大,整理耗时越久

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飙升

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

解决方案:

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

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

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

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

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

解决方案:

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

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

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

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

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

解决方案:

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

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

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

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

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

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

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

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

七、总结

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

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

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

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

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

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询