垃圾回收-垃圾回收器

垃圾回收器

概述

:::warning

① 垃圾回收器是虚拟机中具体执行垃圾回收的部分,也是垃圾回收算法的具体实现

② 不过后续许多的垃圾回收器并不是严格使用此前提到的算法,有非常多不同的改进

③ Java 社区中的许多公司都开发了自己的虚拟机,诸如 IBM、Oracle、RedHat 等等

④ 不同的公司研发的不同的虚拟机,以及虚拟机的逐步改进,可能会让初看的同学感到混乱

⑤ 所以我打算先把垃圾回收器的发展历史写出来,先了解大致有什么垃圾回收器,再进一步了解

:::

历史

  • 伴随 Java 虚拟机诞生的 [两款串行执行垃圾回收器应该是 Serial 和 Serial Old]{.red}
    • [Serial 主要用于收集年轻代、Serial Old 主要用于收集老年代]{.red}
  • JDK 1.3 推出了第一款能够 [并行执行的垃圾回收器 ParNew]{.red}
    • ParNew 是 Serial 垃圾回收器的多线程版本
  • JDK 1.4 推出了 [高吞吐量的并行执行的垃圾回收器 Parallel Scavenge]{.red}
    • [Parallel Scavenge 主要用于收集年轻代]{.red}
    • Parallel Scavenge 并不是采用 HotSpot 虚拟机规定的分代框架,导致后面无法和 Concurrent Mark Sweep 配合
  • JDK 5 推出了能够 [并发执行的垃圾回收器 Concurrent Mark Sweep]{.red}
    • Concurrent Mark Sweep 是第一款能够实现并发的垃圾回收器
    • [Concurrent Mark Sweep 主要和 ParNew 进行配合]{.red}
  • JDK 6 时推出 [Prarallel Old 垃圾回收器]{.red}
    • Prarallel Old 垃圾回收器主要用于收集老年代
    • [默认使用 Concurrent Mark Sweep + ParNew 的组合]{.red}
  • JDK 7 时 [全能垃圾回收器 Garbage First]{.red} 被推出
  • JDK 8 时 [默认使用 Parallel Scavenge + Parallel Old 组合]{.red}
  • JDK 9 时 使用 Garbage First 垃圾回收器替代此前的组合
    • Concurrent Mark Sweep 垃圾回收器则不被推荐使用
  • JDK 11 时引入 [两款前沿的垃圾回收器 Epsilon 和 ZGC]{.red}
    • Epsilon 是无操作垃圾回收器
    • [ZGC 是 Oralce 自主开发的垃圾回收器,仍然处于实验性质]{.blue}
  • JDK 12 时引入 [Shenandoah 垃圾回收器]{.red}
    • [Shenandoah 垃圾回收器是由 RedHat 开发的]{.blue}
  • JDK 14 时彻底移除 Concurrent Mark Sweep 垃圾回收器

:::info

① 关于这个发展历史,我觉得非常搞笑的一点是,网络上大多数博客提到的都是 JDK 1.3 开始采用第一款垃圾回收器

② 可是 JDK 1.0 到 JDK 1.3 中间有三年的过度,那不可能这三年间虚拟机没有垃圾回收器吧,太扯了

③ 《深入理解虚拟机》中提到 JDK 1.3 之前都是采用的 Serial 垃圾回收器,我觉得还比较合理

:::

分类

:::info

不同的垃圾回收器针对的堆空间区域不同,而且有并行与并发的区别

:::

垃圾回收器分类
  • 按照垃圾回收线程数量分类

    • 垃圾回收的执行过程中可以启用多条垃圾回收线程并行执行,也可以单线程串行执行
    • [串行(单线程):常用于客户端模式的下的虚拟机中;在单核处理器中的表现甚至优于并行执行的效率]{.red}
    • [并行(多线程):常用于服务端模式下的虚拟机中;在多核处理器中的效果较好]{.red}
  • 按照垃圾回收线程和用户线程关系分类

  • 按照工作区域分类

    • 垃圾回收算法中提到了分代算法,针对不同的堆空间区域采用不同的算法
    • 垃圾回收算法的具体实现又是垃圾回收器,所以最早垃圾回收器也是针对不同区域设计的

组合

垃圾回收器组合图示
  • [最早的垃圾回收器组合:Serial + Serial Old]{.blue}
  • [JDK 6 时代的垃圾回收器组合:ParNew + CMS]{.blue}
  • [JDK 8 时代的垃圾回收器组合:Parallel Scavenge + Parallel Old]{.blue}
  • [JDK 9 时代的垃圾回收器:G1]{.blue}

性能指标

:::info

就好比采用时间复杂度衡量算法的好坏一样,需要有相应的标准衡量垃圾回收器性能的好坏

:::

  • 核心指标:

    • [延迟(Latency):垃圾回收过程中用户线程暂停的总时间]{.red}
    • [吞吐量(Throughput):$$\frac{\text{用户线程运行时间}}{用户线程运行时间 + 垃圾回收时间}$$]{.red}
    • [内存占用(Footprint):堆空间的大小]{.red}
  • 不可能三角(类似于分布式系统中的 CAP 理论)

    • 垃圾回收器想要减少 STW 带来的延迟影响就只能够允许并发,将整个垃圾回收过程分成几个阶段进行

      但是分阶段之后就会造成吞吐量的下降,所以这两个条件是很难一起满足的

    • 虚拟机想要使用更大的堆空间,那么回收的过程需要垃圾回收器扫描的区域就变大了,耗费的时间就变长了

      STW 带来的延迟影响就更加明显,所以这两个条件也是非常难以同时满足的

    • 总结:[最优秀的垃圾回收器也最多只能够满足其中的两个条件,是无法同时满足三个条件的]{.red}

    • 问题:可能初学的时候会觉得延迟和吞吐量并不是矛盾的,无法理解为什么分阶段之后就会造成吞吐量下降,这里用图示来演示

      吞吐量与延迟
  • 其余指标:[垃圾回收器为了标记清除垃圾所维护的额外信息占用的内存]{.blue}

    • 可达性分析中维护引用采用的 Oopmap 占用的空间
    • 记录跨代引用的卡表占用的空间,以及维护卡表所需要的操作开销
  • 核心:[未来所有垃圾回收器都是朝着在 确保延迟可控的情况尽最大可能提升吞吐量 的方向努力]{.red}

垃圾回收器

7 大经典垃圾回收器

:::info

① 前面的串行和并行垃圾收集器都是比较简单,算法都是基于此前提到过的基础算法

② 后面的并发和全能回收器的理解具有一定的难度,一些细节是缺失的,算法是有改进的

:::

串行垃圾回收器

Serial 垃圾回收器
  • 历史:JDK 1.3 之前新生代垃圾收集器的唯一选择,是历史最长最基础的垃圾收集器

  • 特点:

    • [针对堆空间中的新生代回收垃圾]{.red}
    • [采用 标记-复制 算法 串行回收 新生代的垃圾]{.red}
      • 标记-复制算法效率相对较高
      • 串行回收不仅造成用户线程长时间的暂停,还因为其单线程所以总的效率相对较低
    • [默认在客户端模式下采用的垃圾回收器,也可以手动设置为服务器端下使用(没啥意义)]{.red}
    • 默认 Serial 和 Serial Old 进行搭配
  • 优点:

    • [所有垃圾收集器中占用额外内存最小的(不需要记录太多的信息辅助垃圾收集)]{.red}
    • [单核处理器下运行效果非常好,相比于其他串行垃圾收集器简单高效]{.red}
      • 可以在桌面应用程序中应用
      • 也可以在微服务中应用(客户端)
  • 缺点:

    • [串行回收会造成长时间的用户线程暂停,效率相对来说较低]{.green}
    • [多核处理器下就明显比不过并行的垃圾回收器,无法应用于服务器端]{.green}
  • 设置命令:[-XX:+UseSerialGC]{.blue}

    • 指定新生代垃圾回收器为 Serial 的同时,老年代默认设置为 Serial Old

      查看-Serial-垃圾回收器
Serial Old 垃圾回收器
  • 历史:伴随 Serial 垃圾收集器诞生的老年代垃圾收集器
  • 特点:
    • [针对堆空间中的老年代回收垃圾]{.red}
    • [采用 标记-整理 算法 串行回收 老年代的垃圾]{.red}
    • [默认在客户端下使用的垃圾回收器,可以设置为服务器端下使用]{.red}
      • 用于服务器端的目的①:[配合 Parallel Scavenge 新生代收集器使用(JDK 9 中废弃)]{.blue}
      • 用于服务器端的目的②:[用于 CMS 出现失败时(Concurrent Mode Failure)的后备垃圾收集器]{.blue}
  • 优点缺点和 Serial 垃圾收集器基本一致

并行垃圾回收器

  • 所有的并行垃圾回收器都可以设置并行的 GC 线程数量
  • [开启的 GC 线程数量最好不要超过处理器计算核的数量,避免引起线程并发带来的性能下降问题]{.red}
ParNew 垃圾回收器
  • 历史:JDK 1.3 推出第一款并行的垃圾回收器
  • 特点:
    • [针对堆空间中的新生代回收垃圾]{.red}
    • [采用 标记-复制 算法 并行回收 新生代的垃圾]{.red}
    • [默认在服务器端下采用的垃圾回收器,默认开启的线程数量和服务器的计算核数量一致]{.red}
    • JDK 6 默认 ParNew 和 CMS 进行搭配使用
  • 优点:[能够非常高效地利用多核处理器的计算优势,尽快地完成垃圾回收]{.red}
  • 缺点:[单核处理器下的垃圾回收效率并没有 Serial 垃圾回收的效果好]{.green}
  • 细节:ParNew 垃圾回收器除了是多线程并行之外,和 Serial 没有太多的区别
  • 设置命令:
    • [-XX:UseParNewGC 启用 ParNew 垃圾回收器]{.blue}
    • [-XX:ParallelGCThreads 设置 ParNew 启用的垃圾回收线程数量]{.blue}
Parallel Scavenge 垃圾回收器
  • 历史:JDK 1.4 推出,采用不同于 HotSpot 虚拟机规定的垃圾回收器框架,导致该垃圾回收器非常“特别”

  • 特点:

    • [针对堆空间中的新生代回收垃圾]{.red}
    • [采用 标记-复制 算法并行垃圾回收新生代的垃圾]{.red}
    • [唯一 优先确保吞吐量 的垃圾回收器,其余垃圾回收器几乎都是优先确保低延迟]{.red}
      • 开发者可以通过设置 [预期延迟时间]{.red} 控制垃圾回收器的吞吐量大小

      • 开发者也可以通过设置 [垃圾回收时间占比]{.red} 控制吞吐量大小(默认值为 )

        :::info

        ① Parallel Scavenge 和 Garbage First 垃圾回收器都可以设置预期延迟时间,只不过两者的控制方式不一致

        ② 前者为了达到预期时间是通过缩小新生代空间完成的,后者是通过减少回收的内存区域完成的

        ③ 两者的相同点是都会造成吞吐量的下降,只不过后者下降得更少

        :::

    • [采用自适应调节策略:虚拟机自主对内存空间进行调优]{.red}
      • 在开发者对调优细节并不了解的情况下可以采用这种方式
      • 堆空间中伊甸园区和幸存者区默认比例是 8:1:1,采用了自适应策略实际采用的比例并不是 8:1:1
    • [默认在服务器端下采用的垃圾回收器]{.red}
      • 服务器端不需要太多和用户(开发者)交互的时间,需要处理大量的请求数据,高吞吐量的垃圾回收器更合适
    • JDK 8 默认采用 Parallel Scavenge 和 Parallel Old 垃圾回收器配合
  • 优点

  • 缺点:[采用的垃圾回收器框架不同,导致无法和并发垃圾回收器 CMS 配合使用]{.green}

  • 设置命令

    • [-XX:+UseParallelGC 采用 Parallel Scavenge 新生代垃圾回收器]{.blue}
  • 开启 Parallel Scavenge 垃圾回收器的同时默认开启 Parallel Old 老年代垃圾回收器

    • [-XX:ParallelGCThreads=count 设置垃圾回收器开启的线程数量]{.blue}
    • 处理器计算核的数量不超过 8 个的时候:[默认 GC 线程的数量等于处理器计算核的数量]{.red}
      • 处理器计算核的数量超过 8 个的时候:[默认 GC 线程数量 = 3 + ( 5 * CPU_Count / 8)]{.red}
  • [-XX:MaxGCPauseMillis=time 设置预期延迟时间]{.blue}

      * 垃圾回收器会尽可能达到用户设定预期延迟时间,通过减小吞吐量和新生代空间(不可能三角)
      * 最好不要去设置这个参数
    
  • [-XX:GCTimeRatio=time 设置垃圾回收时间占比]{.blue}

    • 虽然这个参数是这个意思,但是实际设置的参数应该是用户运行时间,然后虚拟机会自动计算出垃圾回收时间占比
    • [默认值为 99,所以垃圾回收时间占比为 $$ \frac{1}{用户运行时间 + 1} $$]{.red}(优先关注吞吐量)
    • [-XX:+UseAdaptiveSizePolicy 开启自适应调节机制]{.blue}
    • 无法利用这个参数直接关闭自适应调节机制

Parallel Old 垃圾回收器
  • 历史:JDK 6 推出的老年代的并行垃圾收集器(《深入理解虚拟机》中提到这是个并发收集器,我觉得是写错了)
  • 特点:
    • [针对老年代进行垃圾回收]{.red}
    • [采用 标记-整理 算法 并行回收 老年代的垃圾]{.red}

并发垃圾回收器

:::primary

参考博客:

CMS垃圾回收器详解

Java虚拟机 —-CMS 垃圾回收器

:::

CMS 垃圾回收器
  • 历史:

    • JDK 5 时期推出的第一款并发式垃圾回收器,真正意义上实现了 GC 线程和用户线程同时执行
    • JDK 6 时默认采用 CMS + ParNew 的组合,无法和新推出的 Parallel Scavenge 配合
    • JDK 14 时直接被 Oracle 官方移除
  • 执行过程(细粒度划分):

    • 标记阶段
      • 初始标记(initial mark)
        • 内容:[标记 GC Roots 直接关联到的对象]{.red}
        • 细节:采用 STW 机制,用户线程依然需要停止,不过初始标记非常迅速,造成的时延很短
      • 并发标记(concurrent mark)
        • 内容:[从 GC Roots 直接关联的对象开始遍历整个对象图]{.red}
        • 细节:不采用 STW 机制,用户线程和 GC 线程同时执行
      • 重新标记(remark)
        • 内容:[修正并发标记过程中因用户线程持续运行出现变动的标记]{.red}
        • 细节:采用 STW 机制,用户线程依然需要停止,不过重新标记速度也相对较快,时延步长
    • 清除阶段
      • 并发清除
        • 内容:[清除所有 没有 被标记的对象,释放内存空间]{.red}
        • 方式:标记-清除 / 标记-整理
        • 细节:不采用 STW 机制,用户线程和 GC 线程同时执行
  • 特点:

    • [针对老年代设计的垃圾回收器]{.red}
    • [采用 标记-清除 + 标记-整理 算法进行 并发式 的垃圾回收]{.red}
      • [CMS 能够在不影响对象分配的情况下,容忍标记清除算法带来的碎片问题]{.red}
      • [CMS 将会在执行 Full GC 的时候,启用标记-整理算法用以消除碎片问题]{.red}
        • [标记-整理算法会移动对象的位置,此时运行的用户线程就无法找到对象,所以采用标记-整理算法时无法并发执行,会产生长时间的 STW]{.red}
      • [CMS 采用增量更新的方式解决并发可达性分析的问题]{.red}
  • 优点:[虽然初始标记和重新标记依旧具有时延,但是整体上仍然有效降低 STW 造成的时延]{.red}

  • 缺点:

    • [需要占用处理器资源执行垃圾回收,将会降低用户线程的吞吐量]{.green}
    • [需要确保并发运行期间有足够的预留内存供用户线程使用]{.green}
      • 因为并发运行时用户线程依然会使用内存空间,所以没有预留空间用户线程就无法运行
      • [如果预留空间不足或者用户线程使用内存的速度超过回收速度,那么就会切换至 Serial Old 执行,将会长时间冻结用户线程执行]{.aqua}
    • [需要确保可达性分析的一致性]{.green}
      • [采用 增量更新 保持可达性分析的一致性]{.aqua}
    • [无法处理浮动垃圾问题]{.green}
      • 浮动垃圾:用户线程在 GC 线程标记阶段之后产生的垃圾对象
      • 细节:只能够等待下次垃圾回收发生时再去回收浮动垃圾
  • 设置命令:

    • [-XX:+UseConcMarkSweep 启用 CMS 垃圾回收器]{.blue}

      • 新生代默认使用 ParNew 垃圾回收器
      • 还会默认将 Serial Old 作为紧急情况下的老年代回收器
    • [-XX:ParallelCMSThreads=count 设置 CMS 使用的 GC 线程数量]{.red}

      • 并发式收集中同样可以启用多条 GC 线程,只不过可能面临交替运行的情况
      • [默认使用的线程数量 = (CPU_Count + 3) / 4]{.red}
      • [多线程占用的处理器资源比例会随着处理器计算核数量的增加而减少]{.red}(建议自己算一算)
        • 意味着处理器计算核数量越多,GC 线程就可以开得更多
        • 计算核数量低于 4 个时,多条 GC 线程就会严重影响用户线程的运行
    • [-XX:CMSInitiatingOccupancyFraction=threshold 设置 CMS 开始回收的阈值]{.blue}

      • JDK 5 以前默认设置的值为 68%,JDK 6 之后默认值为 92%

      • [老年代增长较快的时候应该降低阈值,避免触发紧急情况]{.red}

        [老年大增长较慢时应该提升阈值,避免频繁触发 Major GC]{.red}

    • [-XX:+UseCMSCompactAtFullCollection 执行 Full GC 时启用标记整理算法]{.blue}

      • 默认在 CMS 中开启这个选项
    • [-XX:CMSFullGCBeforeCompaction=count 设置执行几次 Full GC 后才使用标记整理算法]{.blue}

      • 设置为 0 的话就是每次执行 Full GC 都采用标记整理算法
      • 默认值为 0
G1 垃圾回收器

:::primary

参考博客:

详解 JVM Garbage First(G1) 垃圾收集器

:::

概述
  • 历史
    • G1 是垃圾回收器技术发展史上的里程碑,开创了面向局部收集的设计思路和基于 Region 的布局方式
    • JDK 6 中作为实验性质的垃圾回收器被推出
    • JDK 7 中正式作为可商用的垃圾回收器推出
    • JDK 8 中支持回收方法区
    • JDK 9 中作为默认使用的垃圾回收器
  • 核心进化:[在延迟可控的情况下尽最大可能提升吞吐量]{.red}
    • 如何实现延迟可控:
      • 采用启发式垃圾回收的方式
      • 建立可靠的停顿时间模型
    • 回顾此前的垃圾回收器:都无法控制垃圾回收的延迟的,只有 Parallel Scavenge 可以控制吞吐量
  • 特点:
    • [面向具有大内存和多处理器的服务器端]{.red}
    • [针对 整个堆空间(老年代+新生代)进行垃圾回收]{.red}
    • [局部上看采用 标记-复制 算法进行垃圾回收、整体上看可以认为是采用 标记-整理 算法进行垃圾回收]{.red}
      • 标记-复制:将 Region 中存活的对象全部复制到另外的空的 Region 区域中
      • 标记-整理:整个堆空间是由多个相等的 Region 构成的,每次回收可以看做是在移动碎片
    • []{.red}
Region 布局
  • 前提:[不再采用 连续内存分配 的方式为对象分配空间,而是采用 非连续内存 的方式]{.red}
    • 连续内存分配:以前大多数对象通常占据的空间都是连续的
    • 非连续内存分配:现在对象占据的空间都是可以不连续的
  • 核心:[将整个堆空间划分多个大小相等的独立区域(Region)]{.red}
  • 大小:
    • 默认值:[默认 Region 的大小是 1MB]{.red}
    • 设置命令:[-XX:G1HeapRegionSize=size 设置 Region 区域的大小]{.blue}
    • 细节:
      • Region 大小在进程运行期间都是不会发生改变的
      • 通常规定 Region 大小的取值范围在 1MB~32MB 之间且最好是 2 的次幂
      • 实际上设置的 Region 大小超过范围或者不是 2 的次幂也不会报错
  • 分类:
    • 普通 Region:存放普通大小的对象
    • 巨型 Region(Humongous):
      • 定义:
        • 巨型 Region 是多个连续的普通 Region 的组合
        • 起始的 Region 被称为开始巨型、后面的 Region 被称为连续巨型
      • 用途:[存放大小超过 Region 大小一半的巨大对象]{.red}
      • 细节:[如果垃圾回收器找不到多个连续的 Region 组成 Humongous,那么可能需要启动 Full GC]{.red}
  • 记忆集:
    • [每个 Region 都需要维护自己的记忆集]{.red}
    • [每个记忆集不仅维护其他 Region 对自己的引用,而且还维护自己对其他 Region 的引用]{.red}
  • 动态分代
    • 定义:
      • [新生代和老年代可以有多个并不连续的 Region 组成]{.red}
      • [组成新生代和老年代的 Region 数量并不固定,取决于多个参数]{.red}
    • 细节:
      • 新生代初始被分配的空间大小为 5%,随着程序的运行而变化大小
  • TAMS(Top At Mark Start):
    • 定义:[用于划分用户线程使用的空间和 GC 回收的空间的指针]{.red}
    • 原因:用户线程和 GC 线程是并发执行的,所以需要在 Region 中预留一部分空间给用户线程使用
  • 启发式垃圾收集
    • 每次都会将各个 Region 按照回收价值和成本进行排序
    • 依据用户设定的期望停顿时间来选择合适的 Region 组合回收集
  • 停顿时间模型
    • 定义:[支持在指定的 M 毫秒的时间片段内,垃圾回收占用的时间 大概率 不超过 N 毫秒]{.red}
    • 实现:
      • 垃圾回收器每次都会根据回收 Region 消耗的时间以及 Region 记忆集中脏卡的数量等参数决定其价值
      • 每个 Region 的价值被决定之后就会采用启发式垃圾收集从而完成停顿时间模型要求的目标
回收模式
  • 新生代回收(Minor GC / Young GC):
    • 内容:采用标记-复制算法将伊甸园区+幸存者区的 Region 中存活的对象复制到空的 Region 中去
    • 回收条件:[新生代的大小占据整个堆空间的 60% 的时候开始执行新生代回收]{.blue}
  • 混合回收(Mixed GC)
    • 内容:采用标记-复制算法将新生代+老年代 Region 中存活的对象全部复制到空的 Region 中去
    • 回收条件:[老年代的大小占据整个堆空间的 45% 的时候开始执行混合回收]{.blue}
  • 细节:
    • [G1 两种回收模式都会回收新生代,老年代只有混合回收中才会回收]{.red}
    • [G1 混合回收老年代的时候只会依据价值高低回收部分的 Region 而不是全部]{.red}
回收过程
  • 前提:无论采用哪种回收模式,其回收过程都是完全一致的

  • 标记阶段

    • 初始标记(Initial Marking)
      • 内容:[标记 GC Roots 能够直接关联到的对象,并且修改 TAMS 的位置为用户线程留出空间]{.red}
      • 细节:依然需要采用 STW 机制不过耗时非常短
    • 并发标记(Concurrent Marking)
    • 最终标记(Final Marking)
      • 内容:
  • 清除阶段

    • 筛选回收
优点与缺点
  • 优点
  • 缺点
  • 命令
    • [-XX:UseG1GC 启用 G1 垃圾回收器]{.blue}
      • G1 垃圾回收器不需要和任何垃圾回收器搭配
    • [-XX:MaxGCPauseMillis 设置期望的垃圾回收时延]{.blue}
    • [-XX:GCTimeRatio=time 设置垃圾回收时间占比]{.blue}
      • Parallel Scavenge 中提供的参数,默认值为 99
      • G1 中同样提供的参数,默认值为 9
    • []{.blue}

3 大前沿垃圾回收器

Shenandoah 垃圾回收器

ZGC 垃圾回收器

Epsilon 垃圾回收器

Author: Fuyusakaiori
Link: http://example.com/2021/09/28/jvm/garbage-collection/垃圾回收-垃圾回收器/
Copyright Notice: All articles in this blog are licensed under CC BY-NC-SA 4.0 unless stating additionally.