前情提要,如果说垃圾回收算法是内存回收的方法论,那么垃圾收集器就是内存回收的实践者。那么,不同的回收算法,有哪些具体的垃圾收集器?适用哪些场景?让我们带着问题,开始发车!
有哪些垃圾收集器
![图1](https://i-blog.csdnimg.cn/blog_migrate/6a3b474f725c5a3c9990a5ff47cab2f4.png)
如图1所示,展示了七种作用于不同分代的收集器,如果两个收集器之间存在连线,说明它们可以搭配使用。虚拟机所处的区域,则表示它是属于新生代收集器还是老年代收集器。
Serial收集器
Serial收集器 是最基本的收集器,属于 新生代 收集器,单线程执行垃圾回收,必须全程冻结用户线程的运行。
根据新生代的对象特性:绝大部分对象的存活时间很短,很快就被回收的特点。因此,收集算法是使用 标记-复制算法。
因此,垃圾回收不是频繁发生的场景下,单线程没有线程交互的开销,可以更加高效的进行垃圾回收。例如:运行在 Client模式 下的虚拟机,Serial收集器 是很好的选择。
Serial收集器的回收过程如下图所示:
![图2](https://i-blog.csdnimg.cn/blog_migrate/8ceac2d22561e07f6ff80edbf2572c8f.png)
safepoint:为了实现 Stop The World 的功能,JVM需要提供一个机制,让所有的线程可以在某一个时刻同时停下来,这个停下来的时刻就叫做safepoint。
ParNew收集器
ParNew收集器 就是 Serial收集器 的多线程版本,也就是 新生代 收集器,使用 标记-复制算法,执行垃圾回收的过程必须全程冻结用户线程的运行。
除了使用多条线程进行垃圾收集之外,其余行为包括 Serial收集器 可用的所有控制参数、收集算法、Stop The World、对象分配规则、回收策略等都与Serial收集器完全一样。
ParNew收集器 除了多线程收集之外,其他与 Serial收集器 相比并没有太多创新之处,但它却是许多运行在 Server模式 下的虚拟机中首选的 新生代 收集器,其中有一个与性能无关但很重要的原因是,除了Serial收集器外,目前只有它能与 CMS收集器 配合工作。
因此,在多核的场景下,ParNew收集器 相比 Serial收集器更有效率。而且,随着CPU的数量增加,ParNew收集器 的垃圾回收效率呈正比增加。
ParNew收集器回收过程如下图所示:
![图3](https://i-blog.csdnimg.cn/blog_migrate/0641becb4869b94bfa99b923017d72d6.png)
Parallel Scavenge收集器
Parallel Scavenge收集器 是 新生代 收集器,同样使用 标记-复制算法,也是并行的执行垃圾回收,执行垃圾回收的过程必须全程冻结用户线程的运行。
与ParNew最大的不同,它并不关注如何尽可能地缩短垃圾回收时用户线程的冻结时间,它关注的是垃圾回收的吞吐量。
吞吐量 = 运行用户代码时间 /( 运行用户代码时间 + 垃圾收集时间 )
用户线程的冻结时间的缩短是以牺牲吞吐量和新生代空间为代价,两者不可兼得。将新生代的内存空间减少,例如,回收 100MB 内存空间的对象总会比回收 200MB 内存空间的对象更快。然而,这样就会导致更加频繁的触发新生代的垃圾回收。
用户线程冻结的时间越短,越适合进行用户交户的程序,快速的响应速度是良好用户体验的基本条件。高吞吐量意味着高效率地利用CPU时间,更快地完成程序的运算任务,因此 Parallel Scavenge收集器 适合应用在隐藏在后台运行的运算任务。
Parallel Scavenge收集器回收过程如下图所示:
![图3](https://i-blog.csdnimg.cn/blog_migrate/95f755e44996645bffb2d339462ed1dc.png)
Serial Old收集器
Serial Old收集器 是 Serial收集器 的 老年代 版本,同样是单线程执行垃圾回收,必须全程冻结用户线程的运行。
根据老年代的对象特性:经历过多次垃圾回收过程的依然存活对象,下一次大概率依旧存活。因此,收集算法是使用 标记-压缩算法。
Serial Old收集器 的主要意义跟 Serial收集器 一样,在于给 Client模式 下的虚拟机使用。
如果在 Server模式 下,那么它主要还有两大用途:一种用途是在JDK 1.5以及之前的版本中与 Parallel Scavenge收集器 搭配使用,另一种用途就是作为 CMS收集器 的后备预案,在并发收集发生 Concurrent Mode Failure 时使用。
Concurrent Mode Failure:
◉ 是指在执行 CMS GC 的过程中同时用户线程将对象放入老年代,而此时老年代空间不足,这时 CMS 还没有机会回收老年代产生的。
◉ 或者在做 Minor GC 的时候,新生代的 Survivor 区 放不下,需要放入老年代,而老年代也放不下而产生的。
Serial Old收集器回收过程如下图所示:
![图5](https://i-blog.csdnimg.cn/blog_migrate/87843275e2cc13c20be00e84ef802271.png)
Parallel Old收集器
Parallel Old收集器 是 Parallel Scavenge收集器 的 老年代 版本,也是并行的执行垃圾回收,执行垃圾回收的过程必须全程冻结用户线程的运行。
根据老年代的对象特性,Parallel Old收集器 使用 标记-压缩算法。
Parallel Old收集器回收过程如下图所示:
![图6](https://i-blog.csdnimg.cn/blog_migrate/8565888b8b005e43da1278d309eea5be.png)
CMS收集器
CMS(Concurrent Mark Sweep)收集器 属于 老年代 收集,它关注的是如何尽可能地缩短垃圾回收时用户线程的冻结时间。因此, CMS收集器是 并发 的执行垃圾回收,执行垃圾回收的过程 不需要 冻结用户线程的运行,用户线程 和 垃圾回收线程 并发执行。
垃圾回收线程和用户线程并发执行有可能会出现 对象消失,针对这个问题,CMS收集器 采用 增量更新 算法实现。
CMS收集器使用 标记-清除算法。
由于并发运行,CMS收集器 的整个过程比之前的收集器都要复杂,整个过程分为四步:
◉ 初始标记(initial mark),单线程执行,需要 Stop The World,但仅仅把 GC Roots 的直接关联可达的对象给标记一下,由于直接关联对象比较小,所以这里的速度非常快。
◉ 并发标记(concurrent mark),对于初始标记过程所标记的初始标记对象,进行并发追踪标记,此时其他线程仍可以继续工作。此处时间较长,但不停顿。
◉ 重新标记(remark),在并发标记的过程中,由于可能还会产生新的垃圾,所以此时需要重新标记新产生的垃圾。此处执行并行标记,与用户线程不并发,所以依然是 ”Stop The World”,时间比初始标记要长一点。
◉ 并发清除(concurrent sweep),并发清除之前所标记的垃圾。其他用户线程仍可以工作,不需要停顿。
整个过程中最耗费时间的并发标记与并发清除阶段,用户线程和垃圾回收线程可以并发执行,因此整体的垃圾回收效率是低停顿的。
不过,因为并发的特性,CMS收集器 的缺点也是比较明显的。
◉ 标记-清除算法会产生内存碎片。
◉ CMS收集器 的并发能力依赖于CPU资源。
◉CMS收集器无法处理浮动垃圾,可能出现 Concurrent Mode Failure 失败而导致另一次Full GC的产生。
也许这里会有一个疑问,标记-清除算法 会产生内存碎片,那么为什么不替换成其他算法,例如 标记-压缩算法?
因为当执行 并发清除 的时候,标记-压缩算法将会整理内存碎片,导致用户线程使用的内存空间发生位移,用户线程出错。为了保证用户线程能继续执行,前提条件是运行的资源不受影响。因此 标记-清除算法 更加合适。
CMS收集器收集器回收过程如下图所示:
![图7](https://i-blog.csdnimg.cn/blog_migrate/7c0fa7fbb5dbe8376be0f5adc8c83290.png)
G1收集器
G1(Garbage First)收集器,是当前收集器技术发展最前沿的成果之一,它是一款面向服务端应用的垃圾收集器。
G1收集器 的设计目标是取代 CMS收集器,相比于 CMS收集器,G1收集器具有以下几个特点:
◉ G1收集器从整体来看是使用 标记-压缩算法,从局部(两个Region之间)上来看是使用 标记-复制算法,意味着 G1收集器 不会产生内存碎片。
◉ G1收集器的 “Stop The World” 更加可控,G1收集器 在停顿时间上添加了 “可预测机制”,用户可以指定期望停顿时间。
接下来,我们深入了解一下 G1收集器 这些特点是怎么实现的。
一、G1收集器的内存布局
首先,需要引入一个重要的概念:Region
在内存空间的划分中,前面介绍的收集器都是明确区分新生代、老年代和永久代,因为各代的内存空间都是连续的,如下图所示:
![图8](https://i-blog.csdnimg.cn/blog_migrate/89ae2315a83eb718ef58e92c3a2272d4.png)
然而,G1收集器 打破了这个传统,它将整个内存空间划分为一个个大小相等的内存块,每个内存块称为:Region。
Region的大小可以通过参数 -XX:G1HeapRegionSize 设定,取值范围从1M 到 32M,且是2的指数。默认情况,G1收集器 会根据 Heap 大小自动决定。
每一块 Region 都可以充当新生代、老年代和永久代,意味着各代的内存空间不再是连续的,如下图所示:
![图9](https://i-blog.csdnimg.cn/blog_migrate/d316f9bbb1cca38e224277b6041d272e.png)
请注意,在图中标注了 Humongous,它表示那些 Region 存储的是巨大对象,如果一个对象占用的内存空间超过了 Region 内存空间50%以上,G1收集器 就认为这是一个巨大对象。
按照分代算法的概念,这些巨大对象,默认直接会被分配在老年代,但如果它是一个短期存活的巨大对象,就会对垃圾收集器造成负面影响。针对这个问题,因此 G1收集器 划分了一个 Humongous 区域,用来存放巨大对象。如果一个 Humongous 区域装不下一个巨大对象,那么 G1收集器 会寻找连续的H分区来存储。因此,为了能找到连续的 Humongous 区域,有时候不得不启动 Full GC。
二、G1收集器的可预测机制
用户可以通过参数 -XX:MaxGCPauseMillis 设定整个 GC 过程的期望停顿时间,默认值200ms,不过它并不是必定结果,只是期望值。
G1收集器 根据 停顿预测模型 统计计算出来的历史数据来预测本次收集需要选择的 Region 数量,从而尽量满足用户设定的目标停顿时间。
停顿预测模型(Pause Prediction Model)是指能够支持指定在一个长度为 M 的时间片段内,垃圾回收的时间不超过 N 的模型。
停顿预测模型是以衰减标准偏差为理论基础实现的:
// TruncateSeq:一个截断的序列,它只跟踪了序列中的最新的n个元素,继承了AbsSeq
// seq->davg():衰减均值
// sigma():信赖度
// seq->dsd():衰减标准偏差
// confidence_factor():可信度相关系数
// seq->num():踪了序列中的最新元素的个数
// hotspot/src/share/vm/gc_implementation/g1/g1CollectorPolicy.hpp
double get_new_prediction(TruncatedSeq* seq) {
return MAX2(seq->davg() + sigma() * seq->dsd(),
seq->davg() * confidence_factor(seq->num()));
}
// hotspot/src/share/vm/utilities/numberSeq.cpp
void AbsSeq::add(double val) {
if (_num == 0) {
// if the sequence is empty, the davg is the same as the value
_davg = val;
// and the variance is 0
_dvariance = 0.0;
} else {
// otherwise, calculate both
_davg = (1.0 - _alpha) * val + _alpha * _davg;
double diff = val - _davg;
_dvariance = (1.0 - _alpha) * diff * diff + _alpha * _dvariance;
}
}
三、为什么只有 G1收集器可以预测 GC 过程的期望停顿时间?
因为 G1收集器 的内存布局被划分为一系列不需要连续区域,Region 作为单次回收的最小单元,每次垃圾回收到的空间都是 Region 大小的整数倍,这样就可以有计划地避免在整个堆空间收集,更容易控制垃圾回收时间。
G1收集器 通过 全局并发标记(global concurrent marking) 跟踪每个 Region 的价值大小,建立各个 Region 空间的优先级列表,并发标记阶段结束后,G1收集器就会知道哪些 Region 的垃圾对象比较多,优先回收。这就是为什么收集器叫 G1(Garbage-First)的原因。
并发标记的作用就是让 垃圾回收线程 和 用户线程 能够同时进行,然而两种线程并发执行有可能会出现 对象消失,针对这个问题,G1收集器 采用 原始快照 算法进行解决。
四、G1收集器的 GC过程
G1收集器 的 GC模式:
◉ 新生代GC(Young GC)
◉ 并发阶段:为混合阶段提供数据支持,对区域进行标记。
◉ 混合GC(Mixed GC)
◉ Full GC:一般是G1出现问题时发生。
◉ Young GC 在 Eden 区填满时触发,回收后之前所有属于 Eden区 的Region 全部变成未分配空间。
其中最主要的两个模式:新生代GC(Young GC)和 混合GC(Mixed GC)
◉ Young GC:回收新生代里的 Region,通过控制回收新生代的 Region 个数,来控制 Young GC 的时间开销。
◉ Mixed GC:不仅回收新生代里的 Region,还会回收老年代里的 Region,根据 全局并发标记(global concurrent marking) 统计得出回收收益高的若干老年代 Region,根据用户指定的 期望停顿时间 尽可能选择收益高的老年代 Region。
(1)新生代GC(Young GC)
Young GC 主要是对 Eden区 进行GC,整个过程需要 Stop The World,如下图所示:
![图10](https://i-blog.csdnimg.cn/blog_migrate/8c6321ae0ee88e5b80c34e43f324f92b.png)
判断对象是否存活,需要从 GC Roots 作为起始点进行遍历,在Young GC 的时候,我们如何找到 GC Roots 呢?GC Roots 可能于新生代和老年代中,如果全量扫描老年代会很耗费时间,影响性能。
因此,针对这个问题,G1收集器 使用了 RSet(Remembered Set)进行辅助,RSet 记录了 其他Region 中的对象引用 本Region 中对象的关系,属于 points-into 结构(谁引用了我的对象)。
Card Table 是一种 points-out(我引用了谁的对象)的结构,每个 Card 覆盖一定范围的 Heap(一般为512Bytes)。
RSet 基于 Card Table ,每个 Region 会记录下 其他Region 指向自己的指针,并标记这些指针分别在哪些 Card 的范围内。RSet 其实是一个 Hash Table,Key 是别的 Region 的起始地址,Value 是一个集合,里面的元素是 Card Table 的 Index。
RSet 结构如下图所示:
![图11](https://i-blog.csdnimg.cn/blog_migrate/54de1df8348435c45d7e567aa0b0cfbe.png)
(2)混合GC(Mixed GC)
Mixed GC 类似于 Full GC 概念,既会回收新生代的 Region,也会回收老年代的 Region,还有 Humongous 巨大对象的 Region。
触发规则根据参数 -XX:InitiatingHeapOccupancyPercent (默认 45%),当老年代 Region 达到整个堆内存的 45% 时触发 Mixed GC。
其中,Mixed GC有一个重要的概念,它的执行过程类似CMS:
全局并发标记(global concurrent marking)
全局并发标记执行过程分为五个步骤:
◉ 初始标记(initial mark,STW):标记从 GC Root 开始直接可达的对象,这个阶段会执行一次 Young GC ,全程冻结用户线程。
◉ 根区域扫描(root region scan):GC 在初始标记的存活区扫描对老年代的引用,并标记被引用的对象。该阶段与应用程序(非 STW)同时运行,并且只有完成该阶段后,才能开始下一次 STW 新生代垃圾回收。
◉ 并发标记(Concurrent Marking):这个阶段从 GC Root 作为起始点,根据对象之间的引用关系搜索出一条引用链,通过遍历引用链来收集各个 Region 的存活对象信息。
◉ 最终标记(Remark,STW):根据原始快照,标记那些在并发标记阶段发生引用变化的对象,必须 Stop The World,否则在最终标记的过程中可能会发生 对象消失 的问题。
◉ 清除垃圾(Cleanup,STW):识别出有存活对象的 Region 和没有存活对象的 Region,更新Rset,将没有存活对象的 Region收集起来到可分配Region队列。
再说几句
大家是否发现一件有趣的事情,收集器的整个发展历程,其实跟垃圾回收算法的发展十分相似。例如,CMS收集器 回归到最基础的 标记-清除算法,而 G1收集器 进阶到 标记-压缩算法 和 标记-复制算法 的配合使用,就像 分代算法 那样。