JVM系列(六) - JVM垃圾回收器

前言

在以前的几篇博客中,咱们大体介绍了,常见的 垃圾回收算法 及 JVM 中常见的分类回收算法。这些都是从算法和规范上分析 Java 中的垃圾回收,属于方法论。在 JVM 中,垃圾回收的具体实现是由 垃圾回收器Garbage Collector)负责。算法

正文

概述

在了解 垃圾回收器 以前,首先得了解一下垃圾回收器的几个名词。编程

1. 吞吐量

CPU 用于运行用户代码的时间与 CPU 总消耗时间的比值。好比说虚拟机总运行了 100 分钟,用户代码 时间 99 分钟,垃圾回收 时间 1 分钟,那么吞吐量就是 99%后端

吞吐量 = 运行用户代码时间/(运行用户代码时间 + 垃圾回收时间)缓存

2. 停顿时间

停顿时间 指垃圾回收器正在运行时,应用程序 的 暂停时间。对于 独占回收器 而言,停顿时间可能会比较长。使用 并发回收器 时,因为垃圾回收器和应用程序 交替运行,程序的 停顿时间 会变短,可是,因为其 效率 极可能不如独占垃圾回收器,故系统的 吞吐量 可能会较低。多线程

3. GC的名词

3.1. 新生代GC(Minor GC)

指发生在 新生代 的垃圾回收动做,由于 Java 对象大多都具有 朝生夕死 的特性,因此 Minor GC 一般 很是频繁,通常回收速度也比较快。架构

3.2. 老年代GC(Major GC)

指发生在 老年代 的垃圾回收动做,出现了 Major GC,常常会伴随至少一次的 Minor GC(发生这种状况,那么 整个堆 都 GC 一遍,一般称为 Full GC)。Major GC 的速度通常会比 Minor GC 慢 10 倍以上。并发

4. 并发与并行

4.1. 串行(Parallel)

单线程 进行垃圾回收工做,但此时 用户线程 仍然处于 等待状态框架

4.2. 并发(Concurrent)

这里的并发指 用户线程 与 垃圾回收线程 交替执行。异步

4.3. 并行(Parallel)

这里的并行指 用户线程 和多条 垃圾回收线程 分别在不一样 CPU 上同时工做。分布式

垃圾回收算法

1. 根搜索算法

根搜索算法 是从 离散数学 中的图论引入的,程序把全部引用关系看做一张图,从一个节点 GC ROOT 开始,寻找对应的 引用节点,找到这个节点后,继续寻找 这个节点 的 引用节点。当全部的引用节点寻找完毕后,剩余的节点 则被认为是 没有被引用到 的节点,即 无用 的节点。

上图 红色 为无用的节点,能够被 回收。目前 Java 中能够做为 GC ROOT 的对象有:

  1. 虚拟机栈 中引用的对象(本地变量表);

  2. 方法区 中 静态变量 引用的对象;

  3. 方法区 中 常量 引用的对象;

  4. 本地方法栈 中引用的对象(Native 对象)。

基本全部 GC 算法都引用 根搜索算法 这种概念。

2. 标记 - 清除算法

标记-清除算法 从 根集合 进行扫描,对 存活的对象 进行 标记。标记完毕后,再扫描整个空间中 未被标记 的对象进行 直接回收,以下图所示:

标记-清除算法 不须要进行 对象的移动,而且仅对 不存活 的对象进行处理,在 存活 的对象 比较多 的状况下 极为高效。但因为 标记-清除算法 直接回收不存活的对象,并无对还存活的对象进行 整理,所以会致使 内存碎片

3. 复制算法

复制算法 将内存划分为 两个区间,使用此算法时,全部 动态分配 的对象都只能分配在 其中一个 区间(活动区间),而 另一个 区间(空间区间)则是 空闲 的。

复制算法 一样从 根集合 扫描,将 存活 的对象 复制 到 空闲区间。当扫描完毕活动区间后,会的将 活动区间 一次性所有 回收。此时本来的 空闲区间 变成了 活动区间。下次 GC 时候又会重复刚才的操做,以此循环。

复制算法 在存活对象 比较少 的时候,极为高效,可是带来的成本是 牺牲一半的内存空间 用于进行 对象的移动。因此 复制算法 的使用场景,必须是对象的 存活率很是低 才行。最重要的是,咱们须要克服 50% 的 内存浪费

4. 标记 - 整理算法

标记-整理算法 采用 标记-清除算法 同样的方式进行对象的 标记,但在回收 不存活的对象 占用的空间后,会将全部 存活的对象 往 左端空闲空间 移动,并更新对应的指针。

标记-整理 是在 标记-清除 之上,又进行了 对象的移动排序整理,所以 成本更高,但却解决了 内存碎片 的问题。

JVM 为了 优化内存 的回收,使用了 分代回收 的方式。对于 新生代内存 的回收(Minor GC)主要采用 复制算法。而对于 老年代内存 的回收(Major GC),大多采用 标记-整理算法

垃圾回收器

1. 垃圾回收器分类标准

2. 七种垃圾回收器概述

在 JVM 中,具体实现有 SerialParNewParallel ScavengeCMSSerial Old(MSC)Parallel OldG1 等。在下图中,你能够看到 不一样垃圾回收器 适合于 不一样的内存区域,若是两个垃圾回收器之间 存在连线,那么表示二者能够 配合使用

若是当 垃圾回收器 进行垃圾清理时,必须 暂停 其余全部的 工做线程,直到它彻底收集结束。咱们称这种须要暂停工做线程才能进行清理的策略为 Stop-the-World。以上回收器中, SerialParNewParallel ScavengeSerial OldParallel Old 均采用的是 Stop-the-World 的策略。

图中有 7 种不一样的 垃圾回收器,它们分别用于不一样分代的垃圾回收。

  • 新生代回收器:Serial、ParNew、Parallel Scavenge

  • 老年代回收器:Serial Old、Parallel Old、CMS

  • 整堆回收器:G1

两个 垃圾回收器 之间有连线表示它们能够 搭配使用,可选的搭配方案以下:

新生代 老年代
Serial Serial Old
Serial CMS
ParNew Serial Old
ParNew CMS
Parallel Scavenge Serial Old
Parallel Scavenge Parallel Old
G1 G1

3. 单线程垃圾回收器

3.1. Serial(-XX:+UseSerialGC)

Serial 回收器是最基本的 新生代 垃圾回收器,是 单线程 的垃圾回收器。因为垃圾清理时,Serial 回收器 不存在 线程间的切换,所以,特别是在单 CPU 的环境下,它的 垃圾清除效率 比较高。对于 Client 运行模式的程序,选择 Serial 回收器是一个不错的选择。

Serial 新生代回收器 采用的是 复制算法

3.2. Serial Old(-XX:+UseSerialGC)

Serial Old 回收器是 Serial 回收器的 老生代版本,属于 单线程回收器,它使用 标记-整理 算法。对于 Server 模式下的虚拟机,在 JDK1.5 及其之前,它常与 Parallel Scavenge 回收器配合使用,达到较好的 吞吐量,另外它也是 CMS 回收器在 Concurrent Mode Failure 时的 后备方案

Serial 回收器和 Serial Old 回收器的执行效果以下:

Serial Old 老年代回收器 采用的是 标记 - 整理算法

4. 多线程垃圾回收器(吞吐量优先)

4.1. ParNew(-XX:+UseParNewGC)

ParNew 回收器是在 Serial 回收器的基础上演化而来的,属于 Serial 回收器的 多线程版本,一样运行在 新生代区域。在实现上,二者共用不少代码。在不一样运行环境下,根据 CPU 核数,开启 不一样的线程数,从而达到 最优 的垃圾回收效果。对于那些 Server 模式的应用程序,若是考虑采用 CMS 做为 老生代回收器 时,ParNew 回收器是一个不错的选择。

ParNew 新生代回收器 采用的是 复制算法

4.2. Parallel Scavenge(-XX:+UseParallelGC)

和 ParNew 回收同样,Parallel Scavenge 回收器也是运行在 新生代区域,属于 多线程的回收器。但不一样的是,ParNew 回收器是经过控制 垃圾回收 的 线程数 来进行参数调整,而 Parallel Scavenge 回收器更关心的是 程序运行的吞吐量。即一段时间内,用户代码运行时间占 总运行时间 的百分比。

Parallel Scavenge 新生代回收器 采用的是 复制算法

4.3. Parallel Old(-XX:+UseParallelOldGC)

Parallel Old 回收器是 Parallel Scavenge 回收器的 老生代版本,属于 多线程回收器,采用 标记-整理算法Parallel Old 回收器和 Parallel Scavenge 回收器一样考虑了 吞吐量优先 这一指标,很是适合那些 注重吞吐量 和 CPU 资源敏感 的场合。

Parallel Old 老年代回收器 采用的是 标记 - 整理算法

5. 其余的回收器(停顿时间优先)

5.1. CMS(-XX:+UseConcMarkSweepGC)

CMS(Concurrent Mark Sweep) 回收器是在 最短回收停顿时间 为前提的回收器,属于 多线程回收器,采用 标记-清除算法

相比以前的回收器,CMS 回收器的运做过程比较复杂,分为四步:

  1. 初始标记(CMS initial mark)

初始标记 仅仅是标记 GC Roots 内 直接关联 的对象。这个阶段 速度很快,须要 Stop the World

  1. 并发标记(CMS concurrent mark)

并发标记 进行的是 GC Tracing,从 GC Roots 开始对堆进行 可达性分析,找出 存活对象

  1. 从新标记(CMS remark)

从新标记 阶段为了 修正 并发期间因为 用户进行运做 致使的 标记变更 的那一部分对象的 标记记录。这个阶段的 停顿时间 通常会比 初始标记阶段 稍长一些,但远比 并发标记 的时间短,也须要 Stop The World

  1. 并发清除(CMS concurrent sweep)

并发清除 阶段会清除垃圾对象。

初始标记CMS initial mark)和 从新标记CMS remark)会致使 用户线程 卡顿,Stop the World 现象发生。

在整个过程当中,CMS 回收器的 内存回收 基本上和 用户线程 并发执行,以下所示:

因为 CMS 回收器 并发收集停顿低,所以有些地方成为 并发低停顿回收器Concurrent Low Pause Sweep Collector)。

CMS 回收器的缺点:

  1. CMS回收器对CPU资源很是依赖

CMS 回收器过度依赖于 多线程环境,默认状况下,开启的 线程数 为(CPU 的数量 + 3)/ 4,当 CPU 数量少于 4 个时,CMS 对 用户查询 的影响将会很大,由于他们要分出一半的运算能力去 执行回收器线程

  1. CMS回收器没法清除浮动垃圾

因为 CMS 回收器 清除已标记的垃圾 (处于最后一个阶段)时,用户线程 还在运行,所以会有新的垃圾产生。可是这部分垃圾 未被标记,在下一次 GC 才能清除,所以被成为 浮动垃圾

因为 内存回收 和 用户线程 是同时进行的,内存在被 回收 的同时,也在被 分配。当 老生代中的内存使用超过必定的比例时,系统将会进行 垃圾回收;当 剩余内存 不能知足程序运行要求时,系统将会出现 Concurrent Mode Failure,临时采用 Serial Old 算法进行 清除,此时的 性能 将会下降。

  1. 垃圾收集结束后残余大量空间碎片

CMS 回收器采用的 标记清除算法,自己存在垃圾收集结束后残余 大量空间碎片 的缺点。CMS 配合适当的 内存整理策略,在必定程度上能够解决这个问题。

5.2. G1回收器(垃圾区域Region优先)

G1 是 JDK 1.7 中正式投入使用的用于取代 CMS 的 压缩回收器。它虽然没有在物理上隔断 新生代 与 老生代,可是仍然属于 分代垃圾回收器G1 仍然会区分 年轻代 与 老年代,年轻代依然分有 Eden 区与 Survivor 区。

G1 首先将  分为 大小相等 的 Region,避免 全区域 的垃圾回收。而后追踪每一个 Region 垃圾 堆积的价值大小,在后台维护一个 优先列表,根据容许的回收时间优先回收价值最大的 Region。同时 G1采用 Remembered Set 来存放 Region 之间的 对象引用 ,其余回收器中的 新生代 与 老年代 之间的对象引用,从而避免 全堆扫描G1 的分区示例以下图所示:

这种使用 Region 划分 内存空间 以及有 优先级 的区域回收方式,保证 G1 回收器在有限的时间内能够得到尽量 高的回收效率

G1 和 CMS 运做过程有不少类似之处,整个过程也分为 4 个步骤:

  1. 初始标记(CMS initial mark)

初始标记 仅仅是标记 GC Roots 内 直接关联 的对象。这个阶段 速度很快,须要 Stop the World

  1. 并发标记(CMS concurrent mark)

并发标记 进行的是 GC Tracing,从 GC Roots 开始对堆进行 可达性分析,找出 存活对象

  1. 从新标记(CMS remark)

从新标记 阶段为了 修正 并发期间因为 用户进行运做 致使的 标记变更 的那一部分对象的 标记记录。这个阶段的 停顿时间 通常会比 初始标记阶段 稍长一些,但远比 并发标记 的时间短,也须要 Stop The World

  1. 筛选回收

首先对各个 Region 的 回收价值 和 成本 进行排序,根据用户所指望的 GC 停顿时间 来制定回收计划。这个阶段能够与用户程序一块儿 并发执行,可是由于只回收一部分 Region,时间是用户可控制的,并且停顿 用户线程 将大幅提升回收效率。

与其它 GC 回收相比,G1 具有以下 4 个特色:

  • 并行与并发

使用多个 CPU 来缩短 Stop-the-World 的 停顿时间,部分其余回收器须要停顿 Java 线程执行的 GC 动做,G1 回收器仍然能够经过 并发的方式 让 Java 程序继续执行。

  • 分代回收

与其余回收器同样,分代概念 在 G1 中依然得以保留。虽然 G1 能够不须要 其余回收器配合 就能独立管理 整个GC堆,但它可以采用 不一样的策略 去处理 新建立的对象 和 已经存活 一段时间、熬过屡次 GC 的旧对象,以获取更好的回收效果。新生代 和 老年代 再也不是 物理隔离,是多个 大小相等 的独立 Region

  • 空间整合

与 CMS 的 标记—清理 算法不一样,G1 从 总体 来看是基于 标记—整理 算法实现的回收器。从 局部(两个 Region 之间)上来看是基于 复制算法 实现的。

但不管如何,这 两种算法 都意味着 G1 运做期间 不会产生内存空间碎片,回收后能提供规整的可用内存。这种特性有利于程序长时间运行,分配大对象 时不会由于没法找到 连续内存空间 而提早触发 下一次 GC

  • 可预测的停顿

这是 G1 相对于 CMS 的另外一大优点,下降停顿时间 是 G1 和 CMS 共同的关注点。G1除了追求 低停顿 外,还能创建 可预测 的 停顿时间模型,能让使用者明确指定在一个 长度 为 M 毫秒的 时间片断 内,消耗在 垃圾回收 上的时间不得超过 N 毫秒。(后台维护的 优先列表,优先回收 价值大 的 Region)。

参考

周志明,深刻理解Java虚拟机:JVM高级特性与最佳实践,机械工业出版社


欢迎关注技术公众号:零壹技术栈

零壹技术栈零壹技术栈

本账号将持续分享后端技术干货,包括虚拟机基础,多线程编程,高性能框架,异步、缓存和消息中间件,分布式和微服务,架构学习和进阶等学习资料和文章。

相关文章
相关标签/搜索