垃圾收集算法深度对比:标记-清除 vs 复制 vs 标记-整理
一、三大核心算法全景对比
算法特性对比表
| 特性维度 | 标记-清除 | 复制算法 | 标记-整理 |
|---|---|---|---|
| 执行阶段 | 标记 + 清除 | 复制(存活对象) | 标记 + 整理 |
| 内存布局 | 非连续碎片 | 两块等大空间 | 连续紧凑 |
| 暂停时间 | 中等(两阶段) | 短(一次复制) | 长(需移动对象) |
| 空间开销 | 低(位图标记) | 高(需要2倍空间) | 低(原地整理) |
| 吞吐量 | 中等 | 高 | 中等偏低 |
| 内存碎片 | 严重 | 无 | 无 |
| 适用场景 | 老年代/大对象 | 新生代 | 老年代 |
| 代表实现 | CMS的并发标记 | Serial/ParNew | Serial Old/Parallel Old |
二、标记-清除算法(Mark-Sweep)
1. 核心原理与流程
java
复制
下载
// 标记-清除算法伪代码实现
public class MarkSweepGC {
// 第一阶段:标记(Mark)
public void markPhase() {
/*
标记所有可达对象(从GC Roots出发):
1. GC Roots 包括:
- 栈中引用的对象(局部变量表)
- 静态变量引用的对象
- 常量池引用的对象
- JNI引用的对象
2. 标记方式:
- 对象头中设置标记位
- 单独位图(BitMap)标记
*/
for (Object root : gcRoots) {
markRecursive(root);
}
}
private void markRecursive(Object obj) {
if (obj == null || isMarked(obj)) return;
setMarked(obj); // 标记为存活
// 递归标记所有引用对象
for (Object field : getReferences(obj)) {
markRecursive(field);
}
}
// 第二阶段:清除(Sweep)
public void sweepPhase() {
/*
遍历整个堆内存:
1. 回收未标记的对象(垃圾)
2. 清除标记位(为下次GC准备)
3. 空闲内存加入空闲列表
*/
for (MemoryBlock block : heap) {
if (!isMarked(block)) {
free(block); // 释放内存
addToFreeList(block);
} else {
clearMark(block); // 清除标记位
}
}
}
}
2. 内存布局变化
java
复制
下载
// 标记-清除前后的内存状态
public class MemoryLayout {
/*
执行前(堆内存):
+----+----+----+----+----+----+----+----+
| A₁ | B₁ | | C₁ | | D₁ | E₁ | |
+----+----+----+----+----+----+----+----+
↑存活 ↑垃圾 ↑存活 ↑垃圾 ↑存活 ↑存活 ↑垃圾
标记阶段:标记A、C、D、E为存活
清除阶段后:
+----+----+----+----+----+----+----+----+
| A₁ | | | C₁ | | D₁ | E₁ | |
+----+----+----+----+----+----+----+----+
↑空闲 ↑空闲 ↑空闲
问题:产生大量内存碎片!
*/
}
3. 优缺点深度分析
java
复制
下载
public class MarkSweepAnalysis {
/*
✅ 优点:
1. 实现简单,历史最悠久
2. 不需要移动对象,STW时间相对可控
3. 空间开销小(只需要标记位)
4. 适合大对象和存活率高的场景
❌ 缺点:
1. 内存碎片严重(致命问题)
- 导致分配大对象时频繁触发Full GC
- 碎片整理需要额外成本
2. 效率问题:
- 标记和清除都需要遍历整个堆
- 两次STW:标记一次,清除一次
3. 分配效率低:
- 需要维护空闲列表(free list)
- 分配时需要查找合适大小的空闲块
*/
}
4. 实际应用:CMS收集器的并发标记
java
复制
下载
// CMS(Concurrent Mark Sweep)的标记-清除变种
public class CMSAlgorithm {
/*
CMS的并发标记-清除流程:
1. 初始标记(Initial Mark) - STW
标记GC Roots直接关联的对象
2. 并发标记(Concurrent Mark) - 并发
从直接关联对象开始标记整个引用链
3. 重新标记(Remark) - STW
修正并发标记期间的变化
4. 并发清除(Concurrent Sweep) - 并发
清除垃圾对象
5. 并发重置(Concurrent Reset) - 并发
重置状态,准备下次GC
CMS问题:
- 内存碎片导致Full GC
- 并发模式失败(Concurrent Mode Failure)
- CPU敏感(占用CPU资源)
*/
}
三、复制算法(Copying)
1. 核心原理与流程
java
复制
下载
// 复制算法伪代码实现
public class CopyingGC {
private MemoryRegion fromSpace; // 来源空间(正在使用的)
private MemoryRegion toSpace; // 目标空间(空闲的)
private int allocPtr; // 目标空间分配指针
public void collecting() {
/*
1. 交换from和to空间
2. 复制所有存活对象到to空间
3. 整理对象排列(消除碎片)
4. 更新所有引用指向新地址
*/
swapSpaces();
allocPtr = toSpace.start;
// 复制GC Roots直接引用的对象
for (Object root : gcRoots) {
if (root != null) {
root = copyObject(root);
}
}
// 扫描并复制引用的对象
scanAndCopy();
// 清空from空间(整个空间都是垃圾)
fromSpace.clear();
}
private Object copyObject(Object obj) {
if (obj == null) return null;
// 如果对象已经复制过,直接返回新地址
if (isForwarded(obj)) {
return getForwardedAddress(obj);
}
// 复制对象到to空间
int size = getObjectSize(obj);
Object newObj = toSpace.allocate(size);
copyMemory(obj, newObj, size);
// 设置转发地址(用于处理重复引用)
setForwardedAddress(obj, newObj);
return newObj;
}
private void scanAndCopy() {
int scanPtr = toSpace.start;
while (scanPtr < allocPtr) {
Object obj = getObjectAt(scanPtr);
// 复制obj引用的所有对象
for (Object ref : getReferences(obj)) {
if (ref != null && !isInToSpace(ref)) {
Object newRef = copyObject(ref);
updateReference(obj, ref, newRef);
}
}
scanPtr += getObjectSize(obj);
}
}
}
2. 内存布局变化(半区复制)
java
复制
下载
// 复制算法的内存演变
public class CopyingMemoryLayout {
/*
初始状态(堆分为两个等大半区):
From空间:
+----+----+----+----+----+
| A₁ | B₁ | C₁ | D₁ | E₁ |
+----+----+----+----+----+
↑存活 ↑垃圾 ↑存活 ↑垃圾 ↑存活
To空间:全空
复制完成后:
To空间(紧凑排列):
+----+----+----+
| A₂ | C₂ | E₂ |
+----+----+----+
From空间:全部可回收
交换角色:From ↔ To
关键:存活对象被连续排列,无碎片!
*/
}
篇幅限制下面就只能给大家展示小册部分内容了。整理了一份核心面试笔记包括了:Java面试、Spring、JVM、MyBatis、Redis、MySQL、并发编程、微服务、Linux、Springboot、SpringCloud、MQ、Kafc
需要全套面试笔记及答案
【点击此处即可/免费获取】
3. 优缺点深度分析
java
复制
下载
public class CopyingAlgorithmAnalysis {
/*
✅ 优点:
1. 无内存碎片
- 对象紧凑排列,分配简单快速
- 使用指针碰撞(pointer bumping)分配
2. 吞吐量高
- 只处理存活对象,不扫描垃圾
- 适合存活率低的场景(新生代)
3. 实现简单
- 无需维护空闲列表
- 清除阶段只需清空整个半区
❌ 缺点:
1. 空间利用率低(最大问题)
- 需要2倍内存空间
- 实际只能使用50%的堆空间
2. 对象移动开销
- 复制大对象成本高
- 需要更新所有引用
3. 存活率高时效率低
- 如果大部分对象都存活,复制代价大
*/
}
4. 优化变种:Appel式回收
java
复制
下载
// 改进的复制算法:Appel式回收(分代思想)
public class AppelCopyingGC {
/*
针对复制算法空间浪费的改进:
1. 堆内存划分:
+---------------------------+
| Eden | Survivor | Old |
+---------------------------+
2. 内存比例(HotSpot实现):
- 新生代:Eden:Survivor = 8:1:1
- 老年代:占大部分堆空间
3. 晋升机制:
- 对象经过多次GC后晋升到老年代
- 大对象直接进入老年代
4. 优点:
- 内存利用率提升到90%
- 适合对象"朝生夕死"的特点
*/
}
5. 实际应用:Serial/ParNew收集器
java
复制
下载
// HotSpot新生代收集器使用复制算法
public class YoungGenCollector {
/*
新生代GC(Minor GC)流程:
1. Eden区满时触发
2. 将Eden区存活对象复制到Survivor1
3. 将Survivor0区存活对象复制到Survivor1
4. 清空Eden和Survivor0
5. 交换Survivor0和Survivor1角色
晋升条件:
1. 对象年龄达到阈值(默认15)
2. Survivor空间不足
3. 大对象(超过PretenureSizeThreshold)
内存布局(默认比例8:1:1):
+--------------------------------+
| Eden (80%) |
+----------------+---------------+
| Survivor0 (10%)| Survivor1 (10%)|
+----------------+---------------+
*/
}
四、标记-整理算法(Mark-Compact)
1. 核心原理与流程
java
复制
下载
// 标记-整理算法伪代码实现
public class MarkCompactGC {
// 第一阶段:标记(同标记-清除)
public void markPhase() {
for (Object root : gcRoots) {
markRecursive(root);
}
}
// 第二阶段:计算新位置(整理规划)
public void computePhase() {
/*
计算每个存活对象的新地址:
1. 从堆起始位置开始
2. 为每个存活对象计算紧凑后的新地址
3. 记录在对象头或单独表格中
*/
int newAddress = heap.start;
for (MemoryBlock block : heap) {
if (isMarked(block)) {
setNewAddress(block, newAddress);
newAddress += block.size;
}
}
}
// 第三阶段:更新引用
public void updatePhase() {
/*
更新所有引用指向新地址:
1. 遍历所有存活对象
2. 更新对象内部的所有引用字段
3. 需要两次遍历或使用中间数据结构
*/
for (MemoryBlock block : heap) {
if (isMarked(block)) {
for (Reference ref : getReferences(block)) {
if (ref != null && isMarked(ref.target)) {
ref.target = getNewAddress(ref.target);
}
}
}
}
}
// 第四阶段:移动对象(整理)
public void compactPhase() {
/*
将对象移动到新位置:
1. 按顺序移动所有存活对象
2. 保持对象间的相对顺序(可选)
3. 清空剩余空间
*/
for (MemoryBlock block : heap) {
if (isMarked(block)) {
int newAddr = getNewAddress(block);
if (newAddr != block.address) {
moveObject(block, newAddr);
}
}
}
// 清除剩余空间
clearRemainingSpace();
}
}
2. 整理策略对比
java
复制
下载
// 三种不同的整理策略
public class CompactionStrategies {
// 策略1:任意顺序(任意整理)
public void arbitraryCompaction() {
/*
任意移动对象,不考虑原始顺序:
+----+----+----+----+----+----+
| A₁ | B₁ | C₁ | | D₁ | |
+----+----+----+----+----+----+
↓
+----+----+----+----+----+----+
| C₁ | D₁ | A₁ | B₁ | | |
+----+----+----+----+----+----+
优点:实现简单,移动距离短
缺点:破坏对象局部性,缓存不友好
*/
}
// 策略2:线性顺序(滑动整理)
public void slidingCompaction() {
/*
保持对象原始顺序,滑动整理:
+----+----+----+----+----+----+
| A₁ | B₁ | C₁ | | D₁ | |
+----+----+----+----+----+----+
↓
+----+----+----+----+----+----+
| A₁ | B₁ | C₁ | D₁ | | |
+----+----+----+----+----+----+
优点:保持对象局部性,缓存友好
缺点:移动距离可能较长
*/
}
// 策略3:分代整理(分区域)
public void generationalCompaction() {
/*
将堆划分为多个区域,分别整理:
年轻代 → 复制算法
老年代 → 标记-整理
或进一步将老年代分区:
+-------------------------------+
| Region1 | Region2 | Region3 |
+-------------------------------+
每个区域独立整理,减少暂停时间
*/
}
}
3. 内存布局变化
java
复制
下载
// 标记-整理前后的内存状态
public class MarkCompactLayout {
/*
执行前(堆内存):
+----+----+----+----+----+----+----+----+
| A₁ | B₁ | | C₁ | | D₁ | E₁ | |
+----+----+----+----+----+----+----+----+
标记阶段:标记A、B、C、D、E为存活
整理阶段后(滑动整理):
+----+----+----+----+----+----+----+----+
| A₁ | B₁ | C₁ | D₁ | E₁ | | | |
+----+----+----+----+----+----+----+----+
结果:内存连续,无碎片,空间利用率高
*/
}
4. 优缺点深度分析
java
复制
下载
public class MarkCompactAnalysis {
/*
✅ 优点:
1. 无内存碎片
- 对象连续排列,分配效率高
- 适合老年代长期存活对象
2. 空间利用率100%
- 不需要额外空间(相比复制算法)
- 适合内存受限场景
3. 分配简单
- 使用指针碰撞分配
- 不需要空闲列表管理
❌ 缺点:
1. STW时间长(最大问题)
- 需要三次遍历:标记、计算、移动
- 对象移动开销大
2. 实现复杂
- 需要处理引用更新
- 需要额外的地址映射表
3. 局部性问题
- 对象移动破坏原有内存局部性
- 可能影响缓存命中率
*/
}
5. 实际应用:Parallel Old收集器
java
复制
下载
// Parallel Old收集器的标记-整理实现
public class ParallelOldCollector {
/*
Parallel Old工作流程:
1. 初始标记(Initial Mark) - STW
标记GC Roots直接关联的对象
2. 并发标记(Concurrent Mark)
并行标记所有可达对象
3. 最终标记(Final Mark) - STW
完成标记
4. 并行整理(Parallel Compact)
多个线程并行整理:
a) 区域划分:将堆划分为多个区域
b) 并行计算:每个线程计算负责区域的整理
c) 并行移动:并行移动对象
优化点:
1. 并行化减少STW时间
2. 区域划分减少移动距离
3. 增量整理思想
*/
}
五、三种算法综合对比
1. 性能指标量化对比
java
复制
下载
// 假设堆大小1GB,存活对象200MB的场景
public class AlgorithmBenchmark {
/*
测试场景:
- 堆大小:1GB
- 存活对象:200MB(20%存活率)
- 对象平均大小:1KB
标记-清除:
- 标记时间:遍历1GB,标记200MB对象
- 清除时间:遍历1GB,释放800MB空间
- 总STW:~200ms
- 内存碎片:严重,可能无法分配512KB以上对象
复制算法:
- 复制时间:只复制200MB存活对象
- 空间需求:需要2GB(实际只用1GB)
- 总STW:~80ms
- 内存碎片:无
标记-整理:
- 标记时间:遍历1GB,标记200MB对象
- 整理时间:移动200MB对象
- 总STW:~300ms
- 内存碎片:无
*/
}
2. 不同场景下的选择策略
java
复制
下载
// 根据应用特性选择算法
public class AlgorithmSelection {
// 场景1:Web服务器(对象生命周期短)
public void webServerScenario() {
/*
特点:大量临时对象,存活率低(<10%)
推荐:复制算法
原因:
1. 存活率低,复制代价小
2. 高吞吐量要求
3. 分配速度快
实际:新生代使用复制算法
老年代使用标记-清除或标记-整理
*/
}
// 场景2:大数据计算(内存紧张)
public void bigDataScenario() {
/*
特点:内存消耗大,对象存活时间长
推荐:标记-整理
原因:
1. 内存利用率要求100%
2. 避免碎片导致OOM
3. 可接受较长的GC暂停
实际:G1或ZGC的标记-整理变种
*/
}
// 场景3:实时交易系统(低延迟)
public void realTimeScenario() {
/*
特点:对延迟敏感,暂停时间要求<10ms
推荐:标记-清除 + 并发
原因:
1. 避免对象移动的长暂停
2. 并发执行减少STW
3. 配合增量整理解决碎片
实际:CMS或ZGC
*/
}
}
3. 现代GC算法的融合与演进
java
复制
下载
// 现代垃圾收集器的算法融合
public class ModernGCAlgorithms {
// G1收集器:分区域标记-整理
public class G1Collector {
/*
核心思想:将堆划分为多个Region
1. 标记阶段:
- 并发标记所有Region
- 计算每个Region的回收价值
2. 回收阶段:
- 选择回收价值高的Region
- 复制Region内存活对象到空Region
- 清空原Region
本质:区域化的复制算法
优点:可预测的停顿时间
*/
}
// ZGC收集器:染色指针 + 并发整理
public class ZGCCollector {
/*
核心创新:染色指针(Colored Pointers)
1. 并发标记:
- 利用指针颜色位记录标记状态
- 无需停止应用线程
2. 并发整理:
- 使用读屏障(Load Barrier)
- 对象移动时重定向引用
- 增量式移动对象
优点:亚毫秒级停顿,大堆友好
*/
}
// Shenandoah收集器:并发疏散
public class ShenandoahCollector {
/*
核心:并发复制(Concurrent Evacuation)
1. 初始标记 - STW
2. 并发标记
3. 最终标记 - STW
4. 并发复制(核心创新)
5. 更新引用 - STW
关键:允许应用线程与GC线程
同时访问正在移动的对象
*/
}
}
六、面试深度考点
1. 算法选择背后的权衡
java
复制
下载
// 面试常考:为什么不同代用不同算法?
public class GenerationAlgorithmChoice {
/*
问题:为什么新生代用复制,老年代用标记-整理/清除?
答案:
新生代特点:
1. 对象生命周期短(98%朝生夕死)
2. 存活率低(通常<10%)
3. 需要高吞吐量
→ 复制算法优势:
- 只复制少量存活对象
- 无碎片,分配快
- 适合8:1:1的内存划分
老年代特点:
1. 对象生命周期长
2. 存活率高(通常>70%)
3. 内存空间紧张
→ 标记-整理优势:
- 空间利用率100%
- 避免碎片导致的Full GC
- 适合长期存活对象
特殊情况:
1. CMS使用标记-清除:减少STW时间
2. 大对象直接进老年代:避免复制开销
*/
}
2. 碎片问题的终极解决方案
java
复制
下载
// 内存碎片的应对策略
public class FragmentationSolutions {
// 方案1:压缩整理(最彻底)
public void compactionSolution() {
/*
标记-整理算法的变种:
1. 完全压缩:整个堆整理(STW长)
2. 部分压缩:只整理碎片严重区域
3. 增量压缩:分多次完成整理
*/
}
// 方案2:空闲列表合并
public void freeListCoalescing() {
/*
标记-清除的优化:
执行前空闲列表:
[16KB空闲][8KB已用][32KB空闲][64KB已用][16KB空闲]
合并相邻空闲块后:
[48KB空闲][64KB已用][16KB空闲]
减少碎片,但无法消除
*/
}
// 方案3:分区分配(现代方案)
public void regionBasedAllocation() {
/*
G1/ZGC的分区思想:
将堆划分为多个等大Region(如2MB)
每个Region内部无碎片
对象在Region内连续分配
碎片表现为空闲Region,而非Region内部碎片
更容易管理和整理
*/
}
// 方案4:大对象特殊处理
public void largeObjectHandling() {
/*
G1的Humongous Region:
- 大对象(>Region50%)单独分配
- 占用连续多个Region
- 单独回收策略
避免大对象导致的小碎片
*/
}
}
3. 现代JVM的实际应用
java
复制
下载
// HotSpot JVM各收集器的算法使用
public class HotSpotImplementation {
/*
新生代收集器:
- Serial:复制算法
- ParNew:并行复制算法
- Parallel Scavenge:并行复制算法
老年代收集器:
- Serial Old:标记-整理算法
- Parallel Old:并行标记-整理
- CMS:并发标记-清除
全堆收集器:
- G1:分区域复制 + 标记-整理混合
- ZGC:并发标记-整理
- Shenandoah:并发标记-复制
选择建议:
1. 吞吐量优先:Parallel Scavenge + Parallel Old
2. 低延迟优先:ParNew + CMS 或 G1
3. 超大堆:ZGC 或 Shenandoah
4. 兼容性:JDK8用CMS,JDK11+用G1
*/
}
七、总结:算法演进与选择矩阵
1. 算法演进脉络
text
复制
下载
垃圾收集算法演进树:
第一代:标记-清除(1960)
├─ 优点:实现简单
└─ 问题:碎片严重
第二代:复制算法(1970)
├─ 优点:无碎片,吞吐量高
└─ 问题:空间浪费
第三代:标记-整理(1980)
├─ 优点:无碎片,空间利用率高
└─ 问题:STW时间长
第四代:分代收集(1990)
├─ 新生代:复制算法(优化版)
├─ 老年代:标记-清除/整理
└─ 现代GC的基础
第五代:并发收集(2000+)
├─ CMS:并发标记-清除
├─ G1:分区域并发收集
└─ ZGC/Shenandoah:全并发
篇幅限制下面就只能给大家展示小册部分内容了。整理了一份核心面试笔记包括了:Java面试、Spring、JVM、MyBatis、Redis、MySQL、并发编程、微服务、Linux、Springboot、SpringCloud、MQ、Kafc
需要全套面试笔记及答案
【点击此处即可/免费获取】
2. 生产环境选择指南
yaml
复制
下载
# 根据应用场景选择GC算法
高吞吐量应用(批处理、科学计算):
推荐: Parallel Scavenge + Parallel Old
理由: 最大化吞吐量,可接受较长停顿
低延迟应用(Web服务、交易系统):
推荐:
- 堆<8GB: ParNew + CMS
- 堆8-32GB: G1
- 堆>32GB: ZGC或Shenandoah
理由: 追求低停顿,保证响应时间
内存敏感应用(容器环境、云原生):
推荐: Serial GC 或 Epsilon GC
理由: 内存开销小,适合小堆或测试
大数据应用(Spark、Flink):
推荐: G1 或 ZGC
理由: 大堆友好,可预测停顿
3. 关键决策因素
java
复制
下载
public class DecisionFactors {
/*
选择GC算法时考虑:
1. 应用特性
- 对象生命周期分布
- 对象大小分布
- 分配速率
2. 硬件资源
- 堆内存大小
- CPU核心数
- 内存带宽
3. SLA要求
- 最大容忍停顿时间
- 吞吐量要求
- 延迟要求
4. 运维能力
- 团队熟悉程度
- 监控调优能力
- 升级兼容性
黄金法则:没有最好的算法,只有最适合的算法
建议:先用默认配置,根据监控数据调优
*/
}
最终建议:理解三种基础算法是掌握现代GC的基础。在实际工作中,应根据具体应用场景选择合适的收集器和算法组合,并通过监控持续优化。记住:GC调优是科学与艺术的结合,需要理论指导加实践验证。