【从零学习JVM|第九篇】常见的垃圾回收算法和垃圾回收器

发布于:2025-06-19 ⋅ 阅读:(11) ⋅ 点赞:(0)

前言:

我们知道在堆内存中,会有自动的垃圾回收功能,那今天这篇文章将会向你介绍,这个功能实现的方式,还有实现的对象,接下来就由我来给你们详细介绍垃圾回收的算法和实现算法的回收器。

目录

前言:

常见的四种垃圾回收算法

标记清除算法

核心思想

实现过程两个阶段

使用场景

标记整理算法

核心思想

实现过程

使用场景

复制算法

核心思想

实现过程

 使用场景

分代gc算法

核心思想

Minor GC 前的决策流程

 关键步骤详解

检查1:老年代空间 > 新生代总大小

检查2:老年代空间 > 历史平均晋升大小

检查失败:直接触发 Full GC

常见垃圾回收器

年轻代垃圾回收器(Minor GC)

1. Serial 收集器

2. ParNew 收集器

3. Parallel Scavenge 收集器

老年代垃圾回收器(Major GC/Full GC)

1. Serial Old 收集器

2. Parallel Old 收集器

3. CMS(Concurrent Mark-Sweep)收集器

G1垃圾回收器

G1 的垃圾回收过程

主要优点

主要缺点

不适用场景

总结


常见的四种垃圾回收算法

标记清除算法

核心思想

通过 可达性分析 标记存活对象,直接回收未标记对象的内存

实现过程两个阶段
  1. 标记阶段(STW)

    • 从 GC Roots(栈引用、静态变量等)出发,递归遍历对象图。

    • 对存活对象打标记。

  2. 清除阶段(STW)

    • 线性扫描堆内存。

    • 回收未标记对象的内存块(加入空闲链表)。

优点:实现简单,第一阶段标记,第二阶段清除。

缺点:

  • 碎片化内存:内存是连续的,如果在对象被删除之后,就会出现很多细小的内存,如果我们需要很大的内存空间,那么就很可能无法匹配。

很明显,无法做到,因为红色的就是空闲内存但是最大的才4个字节。

  • 分配速度慢:内存碎片化,所以在回收内存之后,会把这段空闲内存加入一个空闲链表,每次分配内存都会遍历整个链表找到合适的位置。
使用场景
  • 老年代回收:CMS 收集器的回收基础

  • 大对象堆:对象存活率高,移动成本大

  • 嵌入式系统:资源受限环境(如 RTOS)

标记整理算法

核心思想

在标记存活对象后,移动对象消除碎片,使空闲内存连续。我们可以把它看作是基于标记清除的一个处理碎片化内存的算法。

实现过程
  1. 标记阶段

    • 从 GC Roots(栈引用、静态变量等)出发,递归遍历对象图。

    • 对存活对象打标记。

  2. 整理阶段(STW)

    • 滑动整理:将存活对象“滑动”到内存一端。

    • 清理:清理掉没有存活对象。

优点:

  • 内存的使用效率高。
  • 没有碎片化内存。

缺点:

  • 整理阶段效率不高,效率是低于标记清除算法的
使用场景
  • 老年代回收:Serial Old, Parallel Old, ZGC

  • 低碎片需求:实时系统、长期运行服务

  • 内存敏感场景:Android ART 的 Foreground GC

复制算法

核心思想

将内存分为两等份,只使用其中一份;GC 时将存活对象复制到另一份空间,清空原空间。

实现过程
  1. 内存划分:堆分为 From 区(当前使用)和 To 区(空闲),创建对象时都只在From区。

  2. 垃圾回收阶段(STW)

    • 从 GC Roots 遍历存活对象

    • 将存活对象复制到 To 区(保持紧凑排列),删除from区对象

    • 交换 From/To 区角色

优点:

  • 不会发生碎片化
  •  回收高效(仅处理存活对象)

缺点:

  • 内存使用率低,每次只使用一半的内存
  • 对象复制开销大
 使用场景
  • 年轻代回收:HotSpot 的 Serial/ParNew/Parallel Scavenge

  • 短命对象场景:对象存活率 < 10% 时最优

  • 小内存区域:JVM 的 Survivor 区

分代gc算法

现代优秀的垃圾回收算法,会将上面的几种垃圾回收算法组合使用,其中应用最广的就是分代垃圾回收。

核心思想

基于 对象的存活时间长久将堆划分为年轻代(活得短),老年代(活得长),对每代应用不同算法。

年轻代:

  • 分为Eden区用于存储新创建的对象。
  • 幸存者区:这里方便观察,分为S0和S1,这个区使用复制算法进行垃圾回收。

实现过程:

  1. 年轻代(Young Generation)

    • 算法复制算法

    • 过程(刚刚创建时)

      • 对象分配在 Eden

      • Eden 满时触发 Minor GC
        存活对象复制到 Survivor 区中的To区,然后修改交换From与To区

  2. 老年代(Old Generation)

    • 算法标记清除 或 标记整理

    • 触发:空间不足时启动 Full GC

详细过程(第一次创建对象):刚创建的对象会放在Eden区,然后当Eden满了,新创建的对象已经放不进去了,就会触发第一次Minor GC清理垃圾,随后把存活的对象放入,幸存者To区。随后把From和To区交换。

在接下来的对象就又可以放入Eden区。

随后Eden区又满了,这个时候就会再次触发Minor GC,此时From区和Eden区都会进行垃圾清理,把存活下的对象放入To区(复制算法)。

那老年代中的对象是怎么放进去的呢?

其实在每一次Minor GC后都会使存活下的对象年龄+1,年龄初始值为0,当年龄达到阈值(最大值15,跟垃圾回收器的种类有关。)就会成功晋升到老年代存储。

进入老年代的条件:

  • 当年龄达到阈值
  • 当幸存者区中的To区无法容纳新的对象时,To区中的对象不管年龄多少都会放入,老年代

其实我们要清楚,每次触发Minor GC后都有可能会晋升对象到老年代,那如果老年代空间不够咋办。下面Minor GC的详细过程过程。

Minor GC 前的决策流程

 关键步骤详解
检查1:老年代空间 > 新生代总大小
  • 目的:确保老年代能容纳最坏情况(整个新生代对象全部存活晋升)

  • 通过条件Old Free > Eden + Survivor

  • 结果:安全执行 Minor GC

检查2:老年代空间 > 历史平均晋升大小
  • 目的:基于历史数据预测本次晋升风险

  • 通过条件Old Free > Avg(Promoted)

  • 结果冒险执行 Minor GC

    • JVM 会尝试 Minor GC,但已准备好后备方案

    • 若晋升时老年代空间不足 → 立即触发 Full GC

检查失败:直接触发 Full GC
  • 条件Old Free < Avg(Promoted)

  • 结果:跳过本次 Minor GC,直接 Full GC

Full GC:同时清理年轻代和老年代

两次判断:本质上也是判断老年代空间还够不够

历史平均晋升大小:把每次晋升的对象大小加起来/个数得到平均大小。

默认情况下年轻代大小远小于老年代

最后如果触发了Full Gc之后还是无法存储我们的新对象,就会OOM。

常见垃圾回收器

垃圾回收器就是垃圾回收算法的具体实现。垃圾回收器分为年轻代和老年代,所以必须成对使用。

年轻代垃圾回收器(Minor GC)

1. Serial 收集器
  • 算法复制算法(单线程)

  • 工作方式

    • 触发 Minor GC 时暂停所有应用线程(Stop-The-World

    • 单线程完成存活对象标记、复制到 Survivor 区/老年代

  • 适用场景

    • 客户端模式(如桌面应用)

    • 单核服务器或小内存应用(<100MB)

  • 优点:实现简单,无线程交互开销

  • 缺点:STW 停顿明显

2. ParNew 收集器
  • 算法复制算法(多线程并行)

  • 工作方式

    • Serial 的多线程版本,需与 CMS 搭配使用

    • 多线程并行执行标记和复制(线程数通过 -XX:ParallelGCThreads 控制)

  • 适用场景

    • 需与 CMS 配合的服务端应用(如 Web 服务)

  • 优点:缩短年轻代 STW 时间

  • 缺点:在单核环境下性能可能不如 Serial

3. Parallel Scavenge 收集器
  • 算法复制算法(多线程并行)

  • 核心目标最大化吞吐量

  • 关键参数(可以手动配置)

    • -XX:MaxGCPauseMillis:最大 GC 停顿时间目标(不保证)

    • -XX:GCTimeRatio:吞吐量目标(默认 99%,即 GC 时间占比 ≤1%)

  • 适用场景

    • 后台计算、批处理任务(如大数据分析)

  • 优点:吞吐量优先,自适应调节堆大小

  • 缺点:停顿时间不稳定

它是JDK8默认的年轻代垃圾回收器,关注吞吐量,可以自动调节堆内存的大小。

老年代垃圾回收器(Major GC/Full GC)

1. Serial Old 收集器
  • 算法标记-整理算法(单线程)

  • 工作方式

    • 单线程 STW 执行标记、整理(压缩内存消除碎片)

  • 用途

    • 客户端模式下的老年代回收

    • 作为 CMS 失败时的后备方案

2. Parallel Old 收集器
  • 算法标记-整理算法(多线程并行)

  • 工作方式

    • Parallel Scavenge 的老年代搭档

    • 多线程并行标记和整理

  • 目标与 Parallel Scavenge 协同最大化吞吐量

  • 适用场景

    • 高吞吐量要求的服务端应用(如数据仓库)

3. CMS(Concurrent Mark-Sweep)收集器
  • 算法标记-清除算法(并发执行)

  • 目标最小化暂停时间

  • 执行流程

    1. 初始标记(STW):标记 GC Roots 直接关联的对象(速度快)

    2. 并发标记(与应用并行):遍历对象图

    3. 重新标记(STW):修正并发标记期间的引用变化

    4. 并发清除(与应用并行):回收垃圾

  • 优点:大部分工作并发执行,停顿时间短

  • 缺点

    • 内存碎片:需定期 Full GC(Serial Old)整理

    • CPU 敏感:并发阶段占用线程资源

    • 浮动垃圾:并发清理时新产生的垃圾需下次回收

G1垃圾回收器

JDK9之后的默认垃圾回收器,Parallel Scavenge关心吞吐量,CMS关心最大暂停时间,G1的设计就是把他们的优点进行融合。

G1的整个堆被划分为多个相同大小的区域,称为Region,区域不要求连续,分为Eden,Survivor,Old区,Region的大小通过堆空间大小/2048的到。

G1 的垃圾回收过程

G1 的回收过程分为以下四个阶段,部分阶段需要 “Stop The World”(STW):

  1. 初始标记(Initial Mark)

    • STW 阶段,标记 GC Roots 直接引用的对象。
    • 耗时短,仅需扫描根对象和年轻代的 Region。
  2. 并发标记(Concurrent Mark)

    • 与应用程序并发执行,从 GC Roots 开始遍历所有可达对象。
    • 过程中会记录对象图的变化(通过 SATB 算法),避免漏标。
  3. 重新标记(Remark)

    • STW 阶段,处理并发标记期间的对象图变化,确保标记完整性。
    • 采用增量更新算法,耗时比 CMS 更短。
  4. 筛选回收(Cleanup & Evacuation)

    • 计算每个 Region 的垃圾占比,根据停顿时间目标选择价值最高的 Region 进行回收。
    • 移动存活对象到新 Region,释放旧 Region 空间,实现内存整理(无碎片化)。
主要优点
  1. 可预测的低延迟

    • 通过-XX:MaxGCPauseMillis参数控制最大停顿时间,优先回收价值高的 Region,避免全堆扫描。
    • 相比 CMS 的 "标记 - 清除" 算法,G1 的 "标记 - 整理" 算法避免了内存碎片化,减少 Full GC 频率。
  2. 分代与分区的结合

    • 逻辑上分代(年轻代 / 老年代),物理上分区(Region),灵活性高。
    • 大对象直接分配到 Humongous Region,避免频繁晋升导致的性能损耗。
  3. 并行与并发处理

    • 标记阶段与应用线程并发执行,减少 STW 时间。
    • 回收阶段采用多线程并行处理,提升效率。
  4. 大内存处理能力

    • 对于堆内存超过 4GB 的应用,G1 的分区设计使其管理效率显著高于 CMS 和 Parallel GC。
主要缺点
  1. 内存占用开销

    • 为实现精确的停顿控制,G1 需要维护 Remembered Set 和 Card Table 等数据结构,增加约 10%-15% 的内存开销。
  2. 算法复杂度高

    • 相比 Parallel GC,G1 的算法更复杂,在小内存场景下可能不如传统回收器高效。
不适用场景
  1. 小内存应用(堆内存 < 2GB)

    • 此时 G1 的内存开销和算法复杂度可能导致性能不如 Serial 或 Parallel GC。
  2. 吞吐量优先的批处理应用

    • 如数据挖掘、科学计算等,对延迟要求不高,Parallel GC 可能更合适。
  3. 单线程环境

    • G1 的多线程并行优势无法发挥,Serial GC 可能更轻量。

总结:G1 垃圾回收器是现代 Java 应用的首选 GC 方案,尤其适合大内存、低延迟的场景。其核心价值在于通过分区设计和可预测的停顿控制,平衡了吞吐量和响应时间。但在实际应用中,需根据堆内存大小、处理器资源和业务特性合理配置参数,以达到最优性能。

G1垃圾回收器不管是老年代还是年轻代都可以使用,所以不需要搭配使用。

在JDK9的版本之后默认就是它所以也建议使用它。

Region Size必须是2的指数幂,取值范围1M-32M

总结

本文介绍了常见的垃圾回收算法和常见的垃圾回收器,对于他们的优点缺点和使用场景做了一定的介绍,希望通过本篇文章,你可以有所收获。

感谢你的阅读,创作不易,如果对你有帮助希望点赞收藏加转发


网站公告

今日签到

点亮在社区的每一天
去签到