CMS概述

CMS垃圾收集器,在各版本JDK都不是默认的垃圾收集器,但是它开创了并发垃圾收集的新纪元;但其实支撑其发展的理论很久之前就提出了,到实际应用实现经过了数10年之久;这是一个很酷的行为,就像陈奕迅的新歌《孤勇者》;G1也在此基础上承前启后,开启了GC1的新篇章。

CMS垃圾收集器的理念还是分代收集思想,它自己本身是老年代的垃圾收集器,它通过跟新生代的垃圾收集器Serial(jdk9不在支持与CMS一起),ParNew配合来一起工作,实现对整个堆空间的管理。

CMS垃圾收集器是基于标记-清除算法实现的;它的工作工作主要划分为:初始标记(STW),并发标记,重新标记(STW),并发清除四个阶段。其中初始标记和重新标记这两个还是有Stop The World的。

它有以下缺点:

  • 基于标记清除算法,可能造成内存碎片化;

  • 并发阶段时GC线程与工作线程一起工作,可能产生浮动垃圾;

  • 假如并发模式失败,随之兜底的是SerialOld ,那么整个GC的过程将会特别慢。

三色标记算法

很多同学可能会有疑问,为什么CMS可以做到GC线程与工作线程一起并发工作呢?它是如何实现的呢?

其实它的实现主要是依赖于三色标记算法;将对象标记为三种颜色,分别是黑色,灰色和白色。

黑色:表示对象本身及其所有的引用都被扫描过。

灰色:表示对象本身被扫描过,其引用没有被完全扫描。

白色:表示对象本身以及其引用都没有被扫描

image-1655635446609

标记的过程大致如下:

1.刚开始,所有的对象都是白色,没有被访问。

2.将GC Roots直接关联的对象置为灰色。

3.遍历灰色对象的所有引用,灰色对象本身置为黑色,引用置为灰色。

4.重复步骤3,直到没有灰色对象为止。

5.结束时,黑色对象存活,白色对象回收。

但是,并发标记阶段,工作线程跟GC线程同时工作,可能造成引用关系的改变;例如:

  • 黑色指向灰色对象的引用关系不存在了;例如:情况1

  • 灰色指向白色对象的引用关系不存在了,黑色建立了指向白色的引用;例如:情况2

情况1:
image-1655635789380

情况2:
image-1655635848214

针对情况1:
但是由于B已经变为灰色,它仍会被当做存活对象,继续遍历下去。

最终的结果就是本轮GC不会回收B、D、E,留到下次GC时回收,也算是浮动垃圾的一部分,下次GC可以正常处理(浮点垃圾)。

针对情况2:
B到D的引用被切断,且A到D的引用被建立。

此时GC线程继续工作,由于B不再引用D了,尽管A又引用了D,但是因为A已经标记为黑色,GC不会再遍历A了,所以D会被标记为白色,最后被当做垃圾回收。

所以情况2的问题更为严重,D不应该被回收,而被错误的当成垃圾;我们梳理一下,情况2只会发生在灰色到白色的引用关系断开,黑色又建立了针对该白色的引用关系这两个条件同时满足的情况下。

所以针对情况2的问题,有两种不同的解决方案:

1.Incremental Update 称为增量更新;(CMS使用)

2.Snapshot At The begining ,简称SATB;(G1使用)

增量更新:打破的是当黑色对的的引用指向白色对象时,把引用关系记录下来,等扫描结束;再以黑色对象为根,再扫描一次;扫描范围大。

SATB:G1使用的是SATB(Snapshot-At-The-Beginning)的方式,删除的时候记录所有的对象

1、在开始标记的时候生成一个快照图,标记存活对象。

2、在并发标记的时候,把所有旧的引用所指向的对象都变成非白的。

①:对于从gray对象移除的目标引用对象标记为gray,将所有既将被删除的引用关系的旧引用记录下来,最后以这些旧引用为根重新扫描一遍。
②:对于black引用的新产生的对象标记为blcak,将所有新增的引用关系,然
后根据这些引用关系为根重新扫描一遍。

3、可能存在浮动垃圾,将在下次被收集。

G1概述

G1垃圾收集器是继CMS之后,被期盼依旧的明星产品,在JDK9中G1成为默认的垃圾收集器,而CMS被标记为Deprecate,也意味着CMS会逐步退出舞台;G1它仅在逻辑上存在分代的概念,在物理上已经不分代了;对于G1来说,在它整个内存空间不足的时候,也是会发生Full GC的;主要分为Yong GC,Mixed GC,Full GC;

它的工作流程主要也是分为四个阶段:初始标记(STW), 并发标记,最终标记(STW),筛选回收(STW);

Collection Set (CSet):一组可被回收的分区的集合

在Cset中存活的数据会在GC过程中被移动到另一个可用分区Cset的分区可以来自Eden空间,Survivor空间、或者Old空间Cset占用不到整个堆空间1%大小;

Remember Set (RSet):记录了其他Region中的对象到本Region的引用

这样使得垃圾回收器不需要扫描整个堆,来找到谁引用了当前分区中的对象,只需要扫描Rset即可。

但是在每次给对象赋引用时,都需要在Rset做一些额外得记录(做的额外的这些记录,在GC中称为写屏障。注意和jvm中的内存屏障不是一个概念!

G1希望建立在一个“停顿时间模型”的垃圾收集器,基于Region的堆内存布局是它能够实现这个目标的关键;每个Region都可以需要根据需要,扮演不通的分区Eden区、Survivor区、Old区、Hunmongous区(大对象区,超过单个region的50%就是Hunmongous,并且可以占用连续的多个内存)。

G1仍然保留新生代和老年代的概念,但新生代和老年代不再是固定的了,它是将Region作为单次回收的最小单元,即每次收集到的内存空间都是Region大小的整数倍,这样可以有计划地避免在整个Java堆中进行全区域的垃圾收集。

G1收集器可以根据Region里面的垃圾堆积的“价值”大小,价值即回所收获得的空间大小以及回收所需时间的经验值,在后台维护一个优先级列表,每次根据用户设定允许的收集停顿时间,优先处理回收价值收益最大的Region。

G1也会有Full GC,但优化G1的核心就是消除Full GC,或者说减少Full GC。

Card Table

在新生代GC时,如果老年代引用指向新生代对象时,新生代需要扫描全部老年代的引用吗?

答案是不用扫描老年代,而是通过Card Table(卡表)的数据结构来解决该问题;卡表是用来记录老年代引用新生代对象;这样新生代在GC时,可以不用花大量的时间扫描所有年老代对象,来确定每一个对象的引用关系,而可以先扫描卡表,只有卡表的标记位为1时,才需要扫描给定区域的年老代对象。而卡表位为0的所在区域的年老代对象,一定不包含有对新生代的引用。

使用这种方式,可以大大加快新生代的回收速度。原理类似于位图的原理。