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


文章目录

  • 前言
  • [一、 垃圾收集算法全景图](#一、 垃圾收集算法全景图)
  • [二、 标记-清除算法](#二、 标记-清除算法)
    • [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,每一步都在解决特定的痛点。


相关推荐
老毛肚2 小时前
黑马头条 云岚到家
java
码农爱学习2 小时前
使用cJosn读写配置文件
java·linux·网络
庞轩px2 小时前
【无标题】
java·开发语言·jvm
Lyyaoo.2 小时前
【JAVA基础面经】JAVA中的泛型
java
小鱼不会骑车2 小时前
JVM 内存管理与垃圾回收(GC)深度解析
jvm
自然常数e2 小时前
预处理讲解
java·linux·c语言·前端·visual studio
大数据新鸟2 小时前
设计模式详解——模板方法模式
java·tomcat·模板方法模式
无籽西瓜a2 小时前
【西瓜带你学设计模式 | 第四期 - 抽象工厂模式】抽象工厂模式 —— 定义、核心结构、实战示例、优缺点与适用场景及模式区别
java·后端·设计模式·软件工程·抽象工厂模式
always_TT2 小时前
内存泄漏是什么?如何避免?
android·java·开发语言