CMS和G1的优点、缺点、适用场景?|?剧透

优采云 发布时间: 2021-08-12 05:18

  

CMS和G1的优点、缺点、适用场景?|?剧透

  要弄清楚cms和G1,就看这个了

  内容

  在我们开始介绍cms和G1之前,我们可以先剧透一下:

  cms 和 G1 是垃圾采集器中的大杀器。他们需要被理解,并且经常在采访中被问到。

  希望大家带着以下问题阅读,带着目标阅读,收获更多:

  为什么没有像银弹一样适合所有场景的优秀采集器? cms的优缺点及适用场景?为什么cms只能作为老年代的采集器,而不能用于新生代的采集? G1的优缺点和适用场景是什么? 1 cmscollector

  cms(Concurrent Mark Sweep)采集器是一个旨在获得最短恢复暂停时间的采集器。这是因为cmscollector工作时,GC工作线程和用户线程可以并发执行,达到减少采集暂停时间的目的。

  cmscollector 只作用于老年代的采集。它基于标记扫描算法。其操作过程分为4个步骤:

  其中,初始标记和重新标记两个步骤仍然需要Stop-the-world。初始标记只是标记GC Roots可以直接关联的对象。速度非常快。并发标记阶段是GC Roots Tracing的过程,而remarking阶段是为了纠正由于用户程序继续运行而导致的并发标记周期。对于对象变化部分的标记记录,此阶段的暂停时间一般比初始阶段稍长,但比并发标记时间短很多。

  cms以流水线的方式分割采集周期,保持耗时的操作单元与应用线程并发执行。只有那些需要STW的运行单元才能单独进行,控制这些单元在合适的时间运行,才能保证在短时间内完成。这样,在整个采集周期中,只有两次短暂的停顿(初始标记和重新标记),达到近似并发的目的。

  cmscollector 优点:并发采集,低暂停。

  cmsCollector 缺点:

  cms采集器能够实现并发的根本原因在于它采用了“标记-清除”算法,对算法过程进行了细粒度分解。上一章介绍了mark-sweep算法会产生大量的内存碎片,对于新生代来说是不能接受的,所以新生代采集器不提供cms版本。

  另外需要补充的是,JVM挂起的时候,需要选择一个合适的时机。由于JVM系统在运行过程中的复杂性,不可能随时暂停,所以引入了安全点的概念。

  安全点(Safepoint)

  安全点,即程序执行的时候,不可能在所有地方都停下来启动GC,只有到了安全点才可以暂停。 Safepoint的选择既不能太小导致GC等待时间过长,也不能太频繁导致运行时负载过大。

  安全点最初的目的不是停止其他线程,而是寻找一个稳定的执行状态。在这种执行状态下,Java 虚拟机的堆栈不会发生变化。这样,垃圾采集器就可以“安全地”进行可达性分析。只要不离开这个安全点,Java 虚拟机就可以在垃圾回收的同时继续运行这段原生代码。

  程序运行时,不可能在所有地方都停下来启动GC,只有到了安全点才可以暂停。安全点的选择,基本上以程序是否具有允许程序长时间执行的特性为标准。 “长时间执行”最明显的特点就是指令序列的复用,比如方法调用、循环跳转、异常跳转等,所以带有这些函数的指令会产生Safepoint。

  对于安全点,另一个需要考虑的问题是如何让所有线程(不包括执行 JNI 调用的线程)“运行”到最近的安全点,然后在 GC 发生时停止。

  两种解决方案:

  安全区域

  表示在一段代码中,引用关系不会改变。在该区域的任何地方启动 GC 都是安全的。您也可以将安全区域视为扩展的安全点。

  2 G1 采集器

  G1 重新定义了堆空间,打破了原有的生成模型,将堆划分为区域。这样做的目的是采集时不必在整个堆中,这是它最显着的特点。区域划分的好处在于它带来了一个可预测暂停时间的采集模型:用户可以指定采集操作将完成多长时间。也就是说,G1 提供了近乎实时的采集特性。

  G1和cms的特征对比如下:

  功能 G1cms

  并发和生成

  是的

  是的

  最大化堆内存的释放

  是的

  没有

  低延迟

  是的

  是的

  吞吐量

  高

  低

  压缩

  是的

  没有

  可预测性

  强

  弱

  新生代和老年代物理分离

  没有

  是的

  G1 具有以下特点:

  G1 之前其他采集器的采集范围是整个年轻代或年老代,但 G1 不再如此。在堆的结构设计上,G1打破了之前在年轻代或年老代固定采集范围的模式。 G1将堆划分为许多大小相同的区域单元,每个单元称为一个Region。区域是具有连续地址的内存空间。 G1模块的组成如下图所示:

  

  G1 采集器将整个 Java 堆划分为多个大小相等的独立区域(Region)。虽然仍然保留了年轻代和老一代的概念,但年轻代和老一代在物理上已不再分离。它们是一部分 Regions 的集合(不需要是连续的)。 Region的大小是一样的。该值是 1M 到 32M 字节之间的 2 的幂。 JVM 会尝试划分大约 2048 个相同大小的 Region。为此,您可以参考以下源代码。其实这个数字可以手动调整,G1也会根据堆大小自动调整。

  #ifndef SHARE_VM_GC_G1_HEAPREGIONBOUNDS_HPP

#define SHARE_VM_GC_G1_HEAPREGIONBOUNDS_HPP

#include "memory/allocation.hpp"

class HeapRegionBounds : public AllStatic {

private:

// Minimum region size; we won't go lower than that.

// We might want to decrease this in the future, to deal with small

// heaps a bit more efficiently.

static const size_t MIN_REGION_SIZE = 1024 * 1024;

// Maximum region size; we don't go higher than that. There's a good

// reason for having an upper bound. We don't want regions to get too

// large, otherwise cleanup's effectiveness would decrease as there

// will be fewer opportunities to find totally empty regions after

// marking.

static const size_t MAX_REGION_SIZE = 32 * 1024 * 1024;

// The automatic region size calculation will try to have around this

// many regions in the heap (based on the min heap size).

static const size_t TARGET_REGION_NUMBER = 2048;

public:

static inline size_t min_size();

static inline size_t max_size();

static inline size_t target_number();

};

#endif // SHARE_VM_GC_G1_HEAPREGIONBOUNDS_HPP

  G1 采集器之所以能够建立可预测的暂停时间模型,是因为它可以系统地避免整个 Java 堆中的垃圾采集。 G1 将通过合理的计算模型计算和量化每个 Region 的采集成本。这样,在给定“暂停”时间限制的情况下,采集器始终可以选择一组合适的 Region 作为采集。目的就是让其采集开销满足这个约束,从而达到实时采集的目的。

  对于计划从cms或ParallelOld采集器迁移的应用,根据官方推荐,如果发现它们符合以下特征,可以考虑更换为G1采集器以追求更好的性能:

  原文如下:

  如果应用程序具有以下一个或多个特征,现在使用cms 或 ParallelOld 垃圾采集器运行的应用程序将有利于切换到 G1。

  G1集合的操作流程大致如下:

  栈中引用的全局变量和对象可以收录在根集合中,这样在查找垃圾时,可以从根集合中扫描堆空间。在 G1 中,引入了一种可以添加到根集的新类型,即记住集。记忆集(也称为 RSet)用于跟踪对象引用。 G1的很多开源都是从Remembered Set衍生而来的,比如它通常占Heap size的20%左右甚至更多。而且,我们在复制对象的时候,因为需要扫描和更改Card Table信息,这个速度会影响复制的速度,进而影响暂停时间。

  

  卡片表

  有一个场景,老年代的对象可能会引用新生代的对象。在标记幸存对象时,需要扫描老年代的所有对象。因为对象有对新生代对象的引用,所以这个引用也会被称为GC Roots。不需要再做一次全堆扫描吗?成本太高。

  HotSpot 提供的解决方案是一种叫做 Card Table 的技术。该技术将整个堆划分为512字节的卡片,并维护一张卡片表来存储每张卡片的标识位。这个标志表示对应的卡片是否可以有对新生代对象的引用。如果可能,那么我们认为该卡是脏的。

  在执行 Minor GC 时,我们可以在卡片表中查找脏卡,而不是扫描整个老年代,并将脏卡中的对象添加到 Minor GC 的 GC Roots 中。所有脏卡扫描完成后,Java虚拟机将清除所有脏卡的标识位。

  为了保证每一张可能引用新生代对象的卡片都被标记为脏卡片,Java虚拟机需要拦截每个引用类型实例变量的写操作,并进行相应的写标志操作。

  卡表可以减少老年代的全堆空间扫描,可以大大提高GC效率。

  大家可以看看官方文档对G1的outlook(这个英文描述比较简单,我就不翻译了):

  未来:

  G1 计划作为 Concurrent Mark-Sweep Collector (cms) 的长期替代品。将 G1 与 cms 进行比较,有一些差异使 G1 成为更好的解决方案。一个区别是 G1 是一个压缩采集器。 G1 充分压缩以完全避免使用细粒度的空闲列表进行分配,而是依赖于区域。这大大简化了采集器的各个部分,并且在很大程度上消除了潜在的碎片问题。此外,与cms 采集器相比,G1 提供了更多可预测的垃圾采集暂停,并允许用户指定所需的暂停目标。

  3 总结

  查了杜娘的文章关于G1的介绍,文章对G1的介绍大部分都卡在了JDK7或更早的实现中。很多结论已经大大偏离,甚至一些过去的GC选项也不再推荐。例如,JDK9 中的 JVM 和 GC 日志已被重构。例如,PrintGCDetails 已被标记为过时,PrintGCDateStamps 已被删除。指定它会导致JVM无法启动。

  本文对cms和G1的介绍大部分也是基于JDK7的。新版本的内容有一点介绍,但是我没有做过太多介绍(我没有深入研究新版本的JVM),以后有机会可以给个特别的文章专注于介绍。

  4 参考

  《深入了解Java虚拟机》、《热点实战》、《极客时间专栏》

  

0 个评论

要回复文章请先登录注册


官方客服QQ群

微信人工客服

QQ人工客服


线