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、内存碎片等核心问题。


