垃圾收集算法深度解析:从标记-清除到分代收集的演进之路


文章目录

  • 前言
  • [一、 垃圾收集算法全景图](#一、 垃圾收集算法全景图)
  • [二、 标记-清除算法](#二、 标记-清除算法)
    • [2.1 算法原理](#2.1 算法原理)
    • [2.2 算法流程图](#2.2 算法流程图)
    • [2.3 优点分析](#2.3 优点分析)
    • [2.4 缺点剖析------内存碎片](#2.4 缺点剖析——内存碎片)
    • [2.5 应用场景](#2.5 应用场景)
  • [三、 标记-复制算法](#三、 标记-复制算法)
    • [3.1 算法原理](#3.1 算法原理)
    • [3.2 算法流程图](#3.2 算法流程图)
    • [3.3 优点分析](#3.3 优点分析)
    • [3.4 缺点剖析------内存利用率低](#3.4 缺点剖析——内存利用率低)
    • [3.5 应用场景](#3.5 应用场景)
  • [四、 标记-整理算法](#四、 标记-整理算法)
    • [4.1 算法原理](#4.1 算法原理)
    • [4.2 算法流程图](#4.2 算法流程图)
    • [4.3 优点分析](#4.3 优点分析)
    • [4.4 缺点剖析------移动成本高](#4.4 缺点剖析——移动成本高)
    • [4.5 应用场景](#4.5 应用场景)
  • [五、 分代收集算法](#五、 分代收集算法)
    • [5.1 算法原理](#5.1 算法原理)
    • [5.2 分代协作流程图](#5.2 分代协作流程图)
    • [5.3 为什么新生代用复制算法?](#5.3 为什么新生代用复制算法?)
    • [5.4 为什么老年代用标记-整理/清除?](#5.4 为什么老年代用标记-整理/清除?)
  • [六、 深入思考:现代GC的算法演进](#六、 深入思考:现代GC的算法演进)
    • [6.1 G1------Region化分代](#6.1 G1——Region化分代)
    • [6.2 ZGC------染色指针与并发整理](#6.2 ZGC——染色指针与并发整理)
    • [6.3 算法的核心矛盾从未改变](#6.3 算法的核心矛盾从未改变)
  • [七、 总结](#七、 总结)

前言

作为一名拥有Java后端工程师,我们对JVM的垃圾收集(Garbage Collection, GC)并不陌生。然而,面对频繁的Minor GC、偶发的Full GC,以及层出不穷的GC调优参数,我们是否真正理解其背后的算法基石?

垃圾收集算法是JVM内存管理的核心。从最初的标记-清除,到标记-复制,再到标记-整理,直至今天普遍采用的分代收集,每一次算法的演进都是为了解决一个核心矛盾:如何在保证吞吐量的同时,最小化应用暂停时间(Stop-The-World)。

本文将摒弃晦涩的源码,从算法原理出发,深入剖析每种算法的优缺点、适用场景,并结合新生代与老年代的不同特点,解释为什么现代GC采用混合策略。


一、 垃圾收集算法全景图

在深入具体算法之前,我们需要明确一个前提:所有垃圾收集算法都遵循"标记-回收"的基本框架。标记阶段用于识别哪些对象是存活的,回收阶段则负责释放已死对象占用的内存。不同的算法,区别在于回收阶段的实现方式。

算法 核心原理 优点 缺点 经典应用
标记-清除 标记可回收对象,统一清除 实现简单,不需要额外空间 产生内存碎片,分配效率下降 CMS收集器的并发标记阶段
标记-复制 内存平分为两块,只使用一块,存活对象复制到另一块 无碎片,分配效率高 内存利用率低(≤50%) 新生代(Serial、ParNew、Parallel Scavenge)
标记-整理 标记存活对象,向一端移动,清理边界外内存 无碎片,空间利用率高 移动对象成本高,需要暂停应用 老年代(Serial Old、Parallel Old)
分代收集 新生代用复制算法,老年代用标记-整理/清除 综合各算法优势,兼顾吞吐与延迟 实现复杂,需要跨代引用处理 所有主流GC(G1、ZGC、 Shenandoah等)

二、 标记-清除算法

2.1 算法原理

标记-清除算法是最基础的垃圾收集算法,分为两个阶段:

  • 标记阶段:从GC Roots出发,遍历所有可达对象,并标记为存活。
  • 清除阶段:遍历堆内存,回收所有未被标记的对象,释放其占用的空间。

2.2 算法流程图







开始GC
暂停所有应用线程

Stop-The-World
标记阶段

从GC Roots遍历对象图
对象是否可达?
标记对象为存活
对象标记为可回收
继续遍历引用链
所有对象遍历完成?
清除阶段

遍历堆内存
对象是否被标记?
回收内存空间
保留对象,清除标记
产生内存碎片
恢复应用线程
GC结束

2.3 优点分析

  • 实现简单:逻辑直观,不需要额外的内存空间(如复制算法需要预留一块区域)。
  • 兼容性好:适用于对象存活率高的场景,不需要移动对象,对象引用不变。

2.4 缺点剖析------内存碎片

标记-清除最大的问题是内存碎片。清除阶段只是将空闲内存记录在空闲列表中,并不做整理。随着GC的频繁发生,内存中会散布大量不连续的空闲区域。

碎片的危害

  • 分配效率下降:当需要分配一个大对象时,即使总空闲内存足够,也可能因为找不到连续空间而提前触发下一次GC。
  • 加速GC频率:碎片化严重时,JVM被迫更频繁地进行GC来腾出连续空间。

2.5 应用场景

标记-清除算法在现代GC中很少单独使用,但它的思想被广泛借鉴。例如,CMS(Concurrent Mark Sweep)收集器的核心就是并发标记-清除,同时通过空闲列表来管理内存分配。但由于CMS存在碎片问题,最终在JDK9中被标记为废弃。

三、 标记-复制算法

3.1 算法原理

标记-复制算法将内存划分为两个等大的区域(称为From区和To区),每次只使用其中一个区域。当GC发生时:

  1. 暂停应用线程。
  2. 标记From区中的存活对象。
  3. 将存活对象按顺序复制到To区,并紧凑排列。
  4. 清空From区。
  5. 交换From区和To区的角色,保证下一次GC时使用的是新的区域。

3.2 算法流程图

开始GC
暂停应用线程

Stop-The-World
标记阶段

扫描From区存活对象
复制阶段

将存活对象按顺序复制到To区
对象在To区紧凑排列

无碎片
清空From区
交换From和To角色
恢复应用线程
GC结束

3.3 优点分析

  • 无内存碎片:复制过程中实现了内存的"压缩",分配新对象时只需要使用指针碰撞技术,分配效率极高。
  • 回收效率高:当存活对象比例很低时,复制成本很低,只需要复制少量对象即可清空整个区域。

3.4 缺点剖析------内存利用率低

标记-复制算法的致命缺陷是内存利用率不超过50%。因为总有一半的内存空间处于闲置状态,这在内存敏感的系统中是难以接受的。

HotSpot的优化:为了降低内存浪费,HotSpot虚拟机在新生代中没有采用1:1的划分,而是设计了Eden区 + 两个Survivor区(S0和S1) 的结构,默认比例是8:1:1。

  • 实际利用率:新生代可用内存为 Eden + 1个Survivor,总利用率为 90%(8/10 + 1/10)。
  • 担保机制:如果Minor GC后存活对象超过Survivor区大小,则直接晋升到老年代,由老年代兜底。

3.5 应用场景

标记-复制算法是新生代垃圾收集的标配。Serial、ParNew、Parallel Scavenge等新生代收集器都基于此算法。新生代对象"朝生夕死"的特点(存活率通常低于10%)使得复制算法的优势得到最大化发挥。

四、 标记-整理算法

4.1 算法原理

标记-整理算法是标记-清除算法的改进版,它在标记阶段后,不直接清除,而是将存活对象向一端移动,形成一个紧凑的内存块,然后清理边界以外的内存。

4.2 算法流程图

开始GC
暂停所有应用线程

Stop-The-World
标记阶段

从GC Roots遍历对象图
标记所有存活对象
整理阶段

将存活对象向一端移动
对象移动过程中

更新引用地址
存活对象紧凑排列

无碎片
清理边界以外的内存
恢复应用线程
GC结束

4.3 优点分析

  • 无内存碎片:整理后内存连续,分配新对象时可以使用指针碰撞,分配效率高。
  • 空间利用率高:不需要像复制算法那样预留空闲区域,整个内存区域都可以用于对象存储。

4.4 缺点剖析------移动成本高

标记-整理的代价在于移动对象。移动存活对象不仅需要复制数据,还需要更新所有引用这些对象的指针。对于老年代这样对象存活率高的区域,移动成本非常可观。

性能权衡:标记-清除虽然会产生碎片,但不需要移动对象;标记-整理解决了碎片问题,但增加了移动成本。现代GC需要在两者之间寻找平衡。

4.5 应用场景

标记-整理算法主要用于老年代,例如Serial Old和Parallel Old收集器。老年代对象存活率高,不适合复制算法;而标记-清除产生的碎片问题在老年代尤为致命(老年代分配的大对象多,对连续空间要求高),因此标记-整理成为更优选择。

五、 分代收集算法

5.1 算法原理

分代收集不是一种独立的算法,而是一种策略组合。它基于一个观察事实:不同对象的生命周期差异显著。

  • 新生代(Young Generation):对象存活率低,大部分对象朝生夕死。适用标记-复制算法,回收效率高。
  • 老年代(Old Generation):对象存活率高,且多为大对象。适用标记-清除或标记-整理算法,兼顾碎片与效率。

5.2 分代协作流程图

新生代



大对象



对象分配
分配在哪个区域?
Eden区分配
Eden区满?
触发Minor GC

标记-复制算法
存活对象复制到Survivor区
对象年龄达到阈值?
晋升到老年代
继续在Survivor区
直接进入老年代
老年代空间不足?
触发Full GC

老年代采用标记-整理/清除
GC后空间足够?
分配成功
OutOfMemoryError

5.3 为什么新生代用复制算法?

新生代采用复制算法,是基于以下三点考量:

考量维度 分析
对象存活率低 新生代对象平均存活率通常低于10%,复制算法只需要复制这10%的存活对象,效率极高。
分配效率高 复制算法配合TLAB(线程本地分配缓冲区)可以实现无锁的指针碰撞分配,速度极快。
空间利用率可优化 通过8:1:1的Eden:Survivor比例,将内存浪费控制在10%以内,远低于理论上的50%。

5.4 为什么老年代用标记-整理/清除?

老年代采用标记-整理或标记-清除,原因如下:

考量维度 分析
对象存活率高 老年代对象存活率通常超过90%,如果使用复制算法,需要复制大量对象,成本过高。
碎片问题敏感 老年代承载着大对象(如缓存、数组),对内存连续性要求高。标记-清除的碎片可能导致大对象分配失败,因此多数情况下更倾向于标记-整理。
GC频率低 老年代GC频率远低于新生代,即使标记-整理需要移动对象,其整体开销仍在可接受范围内。

六、 深入思考:现代GC的算法演进

分代收集虽然在JDK8及之前的版本中占据统治地位,但随着G1(Garbage First)、ZGC、Shenandoah等现代GC的兴起,垃圾收集算法也在不断演进。

6.1 G1------Region化分代

G1不再将内存物理划分为新生代和老年代,而是划分为若干个等大的Region。每个Region可以扮演Eden、Survivor、Old或Humongous(大对象)的角色。在回收时,G1优先回收垃圾最多的Region(Garbage First的由来),采用标记-复制算法,既避免了碎片,又实现了可控的暂停时间。

6.2 ZGC------染色指针与并发整理

ZGC引入了染色指针技术,实现了并发标记-整理。在整理阶段,ZGC通过指针的额外位信息,实现了对象的并发移动,使得停顿时间不再随堆大小增长而增长(控制在10ms以内)。

6.3 算法的核心矛盾从未改变

尽管现代GC的实现越来越复杂,但垃圾收集算法的核心矛盾始终未变:吞吐量与延迟的权衡,空间利用率与分配效率的平衡。理解标记-清除、标记-复制、标记-整理这三类基础算法,是理解任何复杂GC实现的基础。

七、 总结

算法 核心优势 核心劣势 最佳适用场景
标记-清除 实现简单,不移动对象 产生内存碎片 对象存活率适中、对碎片不敏感的场景(如CMS的并发阶段)
标记-复制 无碎片,分配效率极高 内存利用率低(≤50%) 对象存活率低、分配频繁的场景(新生代)
标记-整理 无碎片,空间利用率高 移动对象成本高 对象存活率高、对连续空间敏感的场景(老年代)
分代收集 综合优势,兼顾效率与空间 实现复杂,需要跨代引用处理 通用场景,所有主流GC的基石

垃圾收集算法的演进史,本质上是一部对内存管理效率的极致追求史。从简单粗暴的标记-清除,到为新生代量身定制的复制算法,再到为解决老年代碎片问题而生的标记-整理,直至今天融合了多种策略的分代收集和Region化GC,每一步都在解决特定的痛点。


相关推荐
2301_803875614 小时前
PHP 中处理会话数组时的类型错误解析与修复指南
jvm·数据库·python
m0_743623924 小时前
c++如何批量修改文件后缀名_std--filesystem--replace_extension【实战】
jvm·数据库·python
MY_TEUCK5 小时前
Sealos 平台部署实战指南:结合 Cursor 与版本发布流程
java·人工智能·学习·aigc
三毛的二哥5 小时前
BEV:典型BEV算法总结
人工智能·算法·计算机视觉·3d
我爱cope5 小时前
【从0开始学设计模式-10| 装饰模式】
java·开发语言·设计模式
2501_914245935 小时前
CSS如何处理CSS变量作用域冲突_利用特定类名重写变量值
jvm·数据库·python
南宫萧幕5 小时前
自控PID+MATLAB仿真+混动P0/P1/P2/P3/P4构型
算法·机器学习·matlab·simulink·控制·pid
朝新_5 小时前
【Spring AI 】图像与语音模型实战
java·人工智能·spring
RH2312116 小时前
2026.4.16Linux 管道
java·linux·服务器
zmsofts6 小时前
java面试必问13:MyBatis 一级缓存、二级缓存:从原理到脏数据,一篇讲透
java·面试·mybatis