本文部分摘自《深入理解 Java 虚拟机第三版》
CMS(Concurrent Mark Sweep)收集器是一种以获取最短回收停顿时间为目标的收集器。由于大部分 Java 应用主要集中在互联网网站以及基于浏览器的 B/S 系统的服务端,这类应用通常会较为关注服务的响应速度,希望系统的停顿时间尽可能少,CMS 收集器就非常符合这类应用的需求
从名字可以知道,CMS 收集器是基于标记 - 清除算法实现的,它的运作过程分为四个步骤:
初始标记(CMS initial mark)
仅仅只是标记一下 GC Roots 能直接关联到的对象,速度很快,需要 Stop The World
并发标记(CMS concurrent mark)
就是从 GC Roots 的直接关联对象开始遍历整个对象图的过程,耗时较长,但不需要停顿用户线程,可与垃圾收集器线程一起并发执行
重新标记(CMS remark)
该阶段是为了修正并发标记期间,因用户程序运作而导致标记产生变动的那一部分对象的标记记录,这个阶段需要 Stop The World,而且停顿时间通常比初始阶段稍长一些,但也远比并发标记阶段的时间短
并发清除(CMS concurrent sweep)
清理删除掉标记阶段判断已经死亡的对象,由于不需要移动存活对象,所有这个阶段可以和用户线程并发执行
由于整个过程中耗时最长的是并发标记和并发清除阶段,而这两个阶段都可以和用户线程并发执行,所以从总体上看,CMS 收集器内存回收过程是与用户线程一起并发执行的
CMS 收集器的主要优点就是:并发收集、低停顿,因此也称 CMS 收集器为并发低停顿收集器。但 CMS 还远未达到完美的程度,它至少有以下四个明显的缺点:
对处理器资源非常敏感
在并发阶段,CMS 虽然不会导致用户线程停顿,但却会因为占用了一部分线程(或者说是处理器的计算能力)而导致应用程序变慢,降低吞吐量。处理器核心数在四个或以上那还好,如果不足四个,CMS 会占用将近一半的运算能力去执行收集器线程,这将导致用户程序的执行效率大幅降低
无法处理浮动垃圾
在 CMS 的并发标记和并发清理阶段,由于用户线程继续运行,因此有可能会有新的垃圾对象产生。但这一部分垃圾对象是出现在标记结束之后,CMS 无法在当次收集中处理掉它们,只能留待下一次垃圾收集在清理,这一部分垃圾就称为浮动垃圾
可能会出现并发失败
同样也是由于垃圾收集阶段用户线程还需持续执行,那就必须预留足够的内存空间供用户线程使用。因此 CMS 收集器不能像其他收集器那样等老年代几乎完全填满再进行收集,而必须预留一部分空间供并发收集时的程序运作使用,这部分空间的大小可以通过 -XX:CMSInitiatingOccu-pancyFraction 参数来设置。如果 CMS 运行期间预留的内存无法满足程序分配对象的需要,就会出现一次并发失败,这时虚拟机不得不启用预备方案:冻结用户线程,临时启用 Serial Old 收集器来重新进行老年代的垃圾收集,导致 Stop The World
大量空间碎片的产生
CMS 是一款基于标记 - 清除算法实现的收集器,这也意味着收集结束时会产生大量空间碎片。为了解决这个问题,CMS 收集器提供了一个 -XX:+UseCMS-CompactAtFullCollection 开关参数,用于在收集结束后做一次内存整理,以及 -XX:CMSFullGCsBefore-Compaction 参数,要求 CMS 收集器在执行若干次不整理空间的 Full GC 之后,下一次 Full GC 前先做一次碎片整理
Garbage First(G1)收集器是一款主要面向服务端应用的垃圾收集器,开创了收集器面向局部收集的设计思路和基于 Region 的内存布局形式。HotSpot 开发团队对 G1 收集器的期望就是能在将来替代 CMS 收集器,所以在 JDK9 发布之日,G1 便宣告取代 Parallel Scavenge 加 Parallel Old 组合,成为服务端模式下的默认垃圾收集器,而 CMS 则沦为不推荐使用
在过去,包括 CMS 在内,垃圾收集的范围要么是整个新生代,要么是整个老年代,再要么是整个 Java 堆。而 G1 可以面向堆内存任何部分来组成回收集(Collection Set,一般简称 CSet)进行回收,衡量标准是哪块内存中垃圾数量最多,回收收益最大,这就是 G1 收集器的 Mixed GC 模式
虽然 G1 也是基于分代收集理论设计,但其对内存布局与其他收集器有明显差异。G1 把连续的 Java 堆划分成多个大小相等的独立区域(Region),每一个 Region 可以根据需要扮演新生代的 Eden 空间、Survivor 空间、老年代空间等等。收集器能对扮演不同角色的 Region 采用不同的策略处理
Region 中还有一类特殊的 Humongous 区域,专门用来存储大对象。只要该对象大小超过一半的 Region 的容量即可判定为大对象。而对于那些超过整个 Region 容量的超级大对象,将会被存放在 N 个连续的 Humongous Region 之中,G1 的大多数行为都把 Humongous Region 作为老年代的一部分来看待
停顿时间模型的意思是能够支持指定在一个长度为 M 毫秒的时间片段内,消耗在垃圾收集上的时间大概率不超过 M 毫秒这么一个目标。G1 收集器作为 CMS 收集器的替代者,自然可以实现这个目标
G1 之所以能建立起可预测的停顿时间模型,是因为它将 Region 作为单词回收的最小单元,即每次收集到的内存空间都是 Region 大小的整数倍,这样可以有计划地避免进行全区域的垃圾收集。G1 收集器还可以跟踪每个 Region 的垃圾堆积的“价值”大小,即回收所获得的空间大小以及所需时间,并在后台维护一个优先级列表,每次根据用户设置的允许收集停顿时间(使用 -XX:MaxGCPauseMillis 指定),优先处理回收价值最大的 Region。这种使用 Region 划分内存空间,以及具有优先级的区域回收方式,保证了 G1 收集器在有限的时间内获取尽可能高的收集效率
G1 收集器的设计理念看似无太多惊人之处,其实有很多关键的细节问题需要解决:
如何解决跨 Region 引用对象?
这个问题的解决思路可以使用之前提到过的记忆集来处理,但由于每个 Region 都要维护自己的记忆集,因此实现更加复杂,而且内存占用负担也更重
并发标记阶段如何保证收集线程与用户线程互不干扰?
对应该问题,CMS 采用增量更新算法解决,而 G1 采用原始快照算法解题。另外,G1 还为每一个 Region 设计了两个名为 TAMS(Top At Mark Start)的指针,用于在并发回收过程中新对象的内存分配。G1 收集器默认在这个地址以上的对象是存活的,不会纳入回收范围
如何建立起可靠的停顿预测模型?
G1 收集器的停顿时间模型是以衰减均值(Decaying Average)为理论基础实现的。衰减均值是指它会比普通的平均值更容易受新数据影响,因此,Region 的统计状态越新,越能决定其回收的价值
G1 收集器的运作过程大致可划分为以下四个步骤:
初始标记
仅仅标记一下 GC Roots 能直接关联的对象,并修改 TAMS 指针的值。该阶段需停顿线程,但耗时很短,而且是借进行 Minor GC 时同步完成的,实际上并没有额外的停顿
并发标记
从 GC Roots 开始对堆中对象进行可达性分析,找出要回收对象。该阶段耗时较长,但可与用户程序并发执行。当扫描完成后,还要重新处理 SATB 记录下在并发时有引用变动的对象
最终标记
用户线程短暂暂停,处理并发阶段结束后遗留下来的少量 SATB 记录
筛选回收
更新 Region 统计数据,对各个 Region 的回收价值和成本进行排序,根据用户所期望的停顿时间制定回收计划,然后把要回收的那一部分 Region 的存活对象复制到空的 Region 中,再清理掉整个旧 Region 的全部空间。这里涉及到存活对象的移动,必须暂停用户线程
G1 和 CMS 都非常关注停顿时间控制,毫无疑问,可以由用户指定期望的停顿时间是 G1 收集器的一大杀手锏。G1 收集器经常被拿来和 CMS 收集器比较,从长远来看,G1 收集器肯定是会取代 CMS 收集器的
除了更先进的设计理念,单从传统的算法理论来看,G1 从整体来看是基于标记 - 整理算法实现,而从局部来看(两个 Region 之间)又是基于标记 - 复制算法实现,这意味着 G1 不会产生内存碎片。但 G1 并非全方面碾压 CMS,G1 由于其复杂的内部细节实现,使得垃圾收集时的内存占用和程序运行时的额外执行负载都要比 CMS 高。使用哪款收集器,往往要针对具体场景才能做定量比较,目前在小内存应用上 CMS 的表现大概率会优于 G1,而在大内存应用上 G1 则占有优势,这个平衡点通常在 6GB ~ 8GB 之间。当然,随着 HotSpot 开发者对 G1 的持续优化,最终胜利的天平必定回向 G1 倾斜
联系客服