【前言】
在了解 垃圾回收器 之前,首先得了解一下垃圾回收器的几个名词。
- 吞吐量
CPU 用于运行用户代码的时间与 CPU 总消耗时间的比值。比如说虚拟机总运行了 100 分钟,用户代码 时间 99 分钟,垃圾回收 时间 1 分钟,那么吞吐量就是 99%。
吞吐量 = 运行用户代码时间/(运行用户代码时间 + 垃圾回收时间)
- 停顿时间
停顿时间 指垃圾回收器正在运行时,应用程序 的 暂停时间。对于 独占回收器 而言,停顿时间可能会比较长。使用 并发回收器 时,由于垃圾回收器和应用程序 交替运行,程序的 停顿时间 会变短,但是,由于其 效率 很可能不如独占垃圾回收器,故系统的 吞吐量 可能会较低。 - 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.1. 串行(Parallel)
单线程 进行垃圾回收工作,但此时 用户线程 仍然处于 等待状态。
4.2. 并发(Concurrent)
这里的并发指 用户线程 与 垃圾回收线程 交替执行。
4.3. 并行(Parallel)
这里的并行指 用户线程 和多条 垃圾回收线程 分别在不同 CPU 上同时工作。
垃圾回收器 - 垃圾回收器分类标准
- 七种垃圾回收器概述
在 JVM 中,具体实现有 Serial、ParNew、Parallel Scavenge、CMS、Serial Old(MSC)、Parallel Old、G1 等。在下图中,你可以看到 不同垃圾回收器 适合于 不同的内存区域,如果两个垃圾回收器之间 存在连线,那么表示两者可以 配合使用。
如果当 垃圾回收器 进行垃圾清理时,必须 暂停 其他所有的 工作线程,直到它完全收集结束。我们称这种需要暂停工作线程才能进行清理的策略为 Stop-the-World。以上回收器中, Serial、ParNew、Parallel Scavenge、Serial Old、Parallel Old 均采用的是 Stop-the-World 的策略。
图中有 7 种不同的 垃圾回收器,它们分别用于不同分代的垃圾回收。 - 新生代回收器:Serial、ParNew、Parallel Scavenge
- 老年代回收器:Serial Old、Parallel Old、CMS
- 整堆回收器:G1
两个 垃圾回收器 之间有连线表示它们可以 搭配使用,可选的搭配方案如下:
新生代 | 老年代 |
Serial | Serial Old |
Serial | CMS |
ParNew | CMS |
Parallel Scavenge | Serial Old |
Parallel Scavenge | Parallel Old |
G1 | G1 |
- 单线程垃圾回收器
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.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.1. CMS(-XX:+UseConcMarkSweepGC)
CMS(Concurrent Mark Sweep) 回收器是在 最短回收停顿时间 为前提的回收器,属于 多线程回收器,采用 标记-清除算法。
相比之前的回收器,CMS 回收器的运作过程比较复杂,分为四步: - 初始标记(CMS initial mark)
初始标记 仅仅是标记 GC Roots 内 直接关联 的对象。这个阶段 速度很快,需要 Stop the World。
- 并发标记(CMS concurrent mark)
并发标记 进行的是 GC Tracing,从 GC Roots 开始对堆进行 可达性分析,找出 存活对象。
- 重新标记(CMS remark)
重新标记 阶段为了 修正 并发期间由于 用户进行运作 导致的 标记变动 的那一部分对象的 标记记录。这个阶段的 停顿时间 一般会比 初始标记阶段 稍长一些,但远比 并发标记 的时间短,也需要 Stop The World。
- 并发清除(CMS concurrent sweep)
并发清除 阶段会清除垃圾对象。
初始标记(CMS initial mark)和 重新标记(CMS remark)会导致 用户线程 卡顿,Stop the World 现象发生。
在整个过程中,CMS 回收器的 内存回收 基本上和 用户线程 并发执行,如下所示:
由于 CMS 回收器 并发收集、停顿低,因此有些地方成为 并发低停顿回收器(Concurrent Low Pause Sweep Collector)。
CMS 回收器的缺点:
- CMS回收器对CPU资源非常依赖
CMS 回收器过分依赖于 多线程环境,默认情况下,开启的 线程数 为(CPU 的数量 + 3)/ 4,当 CPU 数量少于 4 个时,CMS 对 用户查询 的影响将会很大,因为他们要分出一半的运算能力去 执行回收器线程; - CMS回收器无法清除浮动垃圾
由于 CMS 回收器 清除已标记的垃圾 (处于最后一个阶段)时,用户线程 还在运行,因此会有新的垃圾产生。但是这部分垃圾 未被标记,在下一次 GC 才能清除,因此被成为 浮动垃圾。
由于 内存回收 和 用户线程 是同时进行的,内存在被 回收 的同时,也在被 分配。当 老生代 中的内存使用超过一定的比例时,系统将会进行 垃圾回收;当 剩余内存 不能满足程序运行要求时,系统将会出现 Concurrent Mode Failure,临时采用 Serial Old 算法进行 清除,此时的 性能 将会降低。 - 垃圾收集结束后残余大量空间碎片
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 个步骤: - 初始标记(CMS initial mark)
初始标记 仅仅是标记 GC Roots 内 直接关联 的对象。这个阶段 速度很快,需要 Stop the World。
- 并发标记(CMS concurrent mark)
并发标记 进行的是 GC Tracing,从 GC Roots 开始对堆进行 可达性分析,找出 存活对象。
- 重新标记(CMS remark)
重新标记 阶段为了 修正 并发期间由于 用户进行运作 导致的 标记变动 的那一部分对象的 标记记录。这个阶段的 停顿时间 一般会比 初始标记阶段 稍长一些,但远比 并发标记 的时间短,也需要 Stop The World。
- 筛选回收
首先对各个 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)。