经典垃圾收集器(二)

 CMS收集器

CMS(Concurrent Mark Sweep)收集器是⼀种以获取最短回收停顿时间为⽬标的收集器。它⾮常符合在注重⽤户体验的应⽤上使⽤。

CMS(Concurrent Mark Sweep)收集器是 HotSpot 虚拟机第⼀款真正意义上的并发收集器, 它第⼀次实现了让垃圾收集线程与⽤户线程(基本上)同时⼯作。

从名字中的Mark Sweep这两个词可以看出,CMS 收集器是⼀种 “标记-清除”算法实现的,它的 运作过程相⽐于前⾯⼏种垃圾收集器来说更加复杂⼀些。整个过程分为四个步骤:

  • 初始标记(CMS initial mark):暂停所有的其他线程,并记录下直接与GC Roots 相连的对象,速度很快 ;
  • 并发标记(CMS concurrent mark):从GC Roots 的直接关联对象开始遍历整个对象图的过程,这个过程耗时较长但是不需要停顿用户线程,可以与垃圾收集器一起并发运行;
  • 重新标记(CMS remark):为了修正并发标记期间因为⽤户程序继续运⾏⽽导致标记产⽣ 变动的那⼀部分对象的标记记录,这个阶段的停顿时间⼀般会⽐初始标记阶段的时间稍⻓, 远远⽐并发标记阶段时间短;
  • 并发清除(CMS concurrent sweep):清理删除掉标记阶段已经死亡的对象,由于不需要移动存活对象。这个阶段可以与用户线程同时并发。

初始标记(CMS initial mark)和重新标记(CMS remark)仍然需要“Stop The World”。

从整体上来说,CMS收集器的内存回收过程是与用户线程一起并发执行的。 

 主要优点:并发收集、低停顿。

三个明显缺点:

  • 对处理器资源非常敏感
  • 无法处理浮动垃圾
  • 它使⽤的回收算法-“标记-清除”算法会导致收集结束时会有⼤量空间碎⽚产⽣。

Garbage First(G1) 收集器

G1收集器是垃圾收集器技术发展历史上的里程碑式成果它开创了收集器面向局部收集的设计思路和基于Region的内存布局。

G1是一款主要面向服务端应用的垃圾收集器。

作为CMS收集器的替代者和继承人,设计者们希望建立一款能够建立起“停顿时间模型”的收集器。

停顿时间模型:能够支持指定在一个长度为M毫秒的时间片段内,消耗在垃圾收集上的时间大概率不超过N毫秒这样的目标。

怎么实现这个目标呢? 

首先在思想上进行改变,在G1收集器出现之前的所有其他收集器,包括CMS在内,垃圾收集的目标要么是整个新生代(Minor GC),要么是整个老年代(Major GC),再要么是整个Java堆(Full GC)。

G1跳出了这个樊笼,它可以面向堆的任何部分来组成收集(Collection Set,一般简称CSet)进行回收,衡量标准不再是它属于那个分代,而是那块内存中存放的垃圾数量最多,回收收益最大,这就是G1收集器的Mixed GC模式。

G1开创的基于Region的堆内存布局是它能够实现这个目标的关键。虽然G1也遵循分代收集理论设计的,但其堆内存布局与其他收集器有明显的差距;G1不再坚持固定大小以及固定数量的分代区域划分,而是把连续的Java堆划分为多个大小相等的独立区域(Region),每一个Region都可以根据需要,扮演新生代的Eden空间、Survivor空间或者老年代空间。收集器能够对扮演不同角色的Region采用不同的策略去处理。

Region中还有一类特殊的Humongous区域,专门用来存储大对象。G1认为只要大小超过了一个Region容量的一半的对象即可判定为大对象。每个Region的大小可以通过参数-XX:G1HeapRegionSize设定,取值范围为1MB-32MB,且应为2的N次幂。对于那些超过了整个Region的超级大对象,将会被存放在N个连续的Humongous Region之中,G1的大多数行为都把Humongous Region作为老年代的一部分来进行看代。

虽然G1仍然保留新生代和老年代的概念,但是新生代和老年代不再是固定的了,它们都是一系列区域(不需要连续)的动态集合。G1收集器将Region作为单次回收的最小单元,即每次收集到的内存空间都是Region大小的整数倍。更具体的处理思路为:让G1收集器去跟踪各个Region里面的垃圾堆积的”价值“大小,价值即回收所获得的空间大小以及回收所需时间的经验值,然后在后台维护一个优先级列表,每次根据用户设定允许的收集停顿时间优先处理回收价值收益最大的那些Region。

 G1至少还存在以下关键问题需要妥善处理:

  • 将Java堆分成多个独立的Region后,Region里面存在的跨Region引用对象,如何解决?

使用记忆集避免全堆作为GC Roots扫描,但在G1收集器上记忆集的应用其实要复杂很多,它的每个Region都维护有自己的记忆集,这些记忆集会记录下别的Region指向自己的指针,并标记这些指针分别在那些卡页范围内。G1的记忆集在存储结构的本质上是一个hash表,Key:别的Region的起始地址,Value:是一个集合,里面存储的元素是卡表的索引号。

  • 在并发标记阶段如何保证收集线程与用户线程互不干扰的运行?

G1收集器通过原始快照来解决用户线程在标记过程中改变对象的引用关系。此外,G1为每一个Region设计了两个名为TAMS(Top at Mark Start)的指针,把Region中的一部分空间划分出来用于并发回收的过程中的新对象分配,并发回收时新分配的对象地址都必须要在这两个指针位置上。G1收集器默认这个地址以上的对象是被隐式标记过,即默认存活,不纳入回收范围。如果回收的速度赶不上内存分配的速度,G1收集器也要被迫冻结用户线程执行,导致Full GC而产生长时间“Stop The World”

  • 怎样建立起可靠的停顿预测模型? 

G1收集器的停顿预测模型是以衰减均值(Decaying Average)为理论基础来实现的,在垃圾收集过程中,G1收集器会记录每个Region的回收耗时、每个Region记忆集里的脏卡数量等各个可测量的步骤花费成本,并分析得出平均值、标准方差、置信度等统计信息。 

G1收集器的运行过程: 

  • 初始标记:仅仅只是标记一下GC Roots能直接关联到的对象,并且修改TAMS指针,让下一阶段用户线程并发运行时,能够正确地在可用的Region中分配对象。这个阶段需要停顿线程,但耗时很短,而且可用借用进行Minor GC的时候同步完成。所以G1收集器在这个阶段实际并没有额外的停顿;
  • 并发标记:从GC Roots开始对堆中对象进行可达性分析,递归扫描整个堆里面的对象图,找出要回收的对象,这个阶段耗时较长,但可与用户程序并发执行。当对象图扫描完成以后,还要重新处理SATB记录下的在并发时有引用变动的对象;
  • 最终标记:对用户线程做另一个短暂的暂停,用于处理并发阶段结束后仍遗留下来的最后那少量的SATB记录;
  • 筛选回收:负责更新Region的统计数据,对各个Region的回收价值和成本进行排序,根据用户所期望的停顿时间来制定回收计划,可以自由选择多个Region构成回收集,然后把决定回收那一部分Region的存活对象复制到空的Region中,再清理掉整个旧的Region全部空间。这里的操作涉及存活对象的移动,是必须暂停用户线程,由多条收集器线程并行完成的。

 G1从整体上来看是基于”标记-整理“算法来实现的收集器,但从局部(两个Region之间)上看又是基于”标记-复制“算法实现。

G1运行期间不会产生内存空间碎片,垃圾收集完成之后能提供规整的可用内存。

两种收集器比较(CMS、G1)

G1无论是垃圾收集产生的内存占用还是程序运行时的额外执行负载都比CMS要高。

内存占用:

G1和CMS都使用卡表来处理跨代指针,但G1的卡表实现更加复杂,而且堆中的每个Region,无论是扮演新生代角色还是老年代角色,都必须有一份卡表,这导致G1的记忆集和其他内存消耗可能会占整个堆容量的20%甚至更多的内存空间;相对于CMS的卡表就相当简单,只有唯一一份,而且只需要处理老年代代新生代的引用,反过来则不需要。

 执行负载:

都使用了写屏障,CMS用写后屏障来更新维护卡表;G1除了使用写后屏障来进行同样的(由于G1的卡表结构复杂)卡表维护操作外,为了实现原始快照(SATB)算法,还需要使用写前屏障来追踪并发时指针变化情况。相比于增量更新算法,原始快照搜索能够减少并发标记和重新标记阶段的消耗,避免CMS那样最终标记阶段时间过长的缺点,但是用户程序运行过程中确实会产生由跟踪引用变化带来的额外负担。

目前对于小内存应用,CMS的表现大概率要优于G1,而在大内存应用上G1则大多能发挥其优势。 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值