垃圾回收器之CMS和G1的比较

CMS

CMS:以获取最短回收停顿时间为目标的收集器,基于并发“标记清理”实现

过程:

1、初始标记:独占CPU,stop-the-world,仅标记GCroots能直接关联的对象,速度比较快

2、并发标记:可以和用户线程并行执行,通过GCRoots Tracing标记所有可达对象

3、重新标记:独占CPU(STW),stop-the-world,对并发标记阶段用户线程运行产生的垃圾对象进行标记修正,以及更新逃逸对象

4、并发清理:可以和用户线程并行执行,清理在重复标记中被标记为可回收的对象

优点:

1.支持并发收集

2.低停顿,CMS可以控制将耗时的两个stop-the-world操作保持与用户线程恰当的时机并发执行,并且能保证在短时间执行完成,这样就达到了近似并发的目的.

缺点:

1、对CPU非常敏感:在并发阶段虽然不会导致用户线程停顿,但是会因为占用了一部分线程,在CPU资源不足的情况下应用会有明显的卡顿。

2、无法处理浮动垃圾:在最后一步并发清理过程中,用户线程执行也会产生垃圾,但是这部分垃圾是在标记之后,所以只有等到下一次gc的时候清理掉,这部分垃圾叫浮动垃圾。

3、CMS使用“标记-清理”法会产生大量的空间碎片,当碎片过多,将会给大对象空间的分配带来很大的麻烦,往往会出现老年代还有很大的空间但无法找到足够大的连续空间来分配当前对象不得不提前触发一次FullGC,为了解决这个问题CMS提供了一个开关参数,用于在CMS顶不住,要进行FullGC时开启内存碎片的合并整理过程,但是内存整理的过程是无法并发的,空间碎片没有了但是停顿时间变长了。

使用场景

在老年代并不频繁GC的场景下,是比较适用的。

CMS 出现FullGC的原因:

1、年轻代晋升到老年代没有足够的连续空间,很有可能是内存碎片导致的

2、在并发过程中JVM觉得在并发过程结束之前堆就会满,需要提前触发FullGC

G1(JDK1.9默认回收器)

G1收集器的内存结构完全区别去CMS,弱化了CMS原有的分代模型(分代可以是不连续的空间),将堆内存划分成一个个Region,这么做的目的是在进行收集时不必在全堆范围内进行。它主要特点在于达到可控的停顿时间,用户可以指定收集操作在多长时间内完成,即G1提供了接近实时的收集特性。

过程:

1.初始标记(Initial Marking):标记一下GC Roots能直接关联到的对象,伴随着一次普通的Young GC发生,并修改NTAMS(Next Top at Mark Start)的值,让下一阶段用户程序并发运行时,能在正确可用的Region中创建新对象,此阶段是stop-the-world操作。


2.根区间扫描,标记所有幸存者区间的对象引用,扫描 Survivor到老年代的引用,该阶段必须在下一次Young GC 发生前结束。


3.并发标记(Concurrent Marking):是从GC Roots开始堆中对象进行可达性分析,找出存活的对象,这阶段耗时较长,但可与用户程序并发执行,该阶段可以被Young GC中断。


4.最终标记(Final Marking):是为了修正并发标记期间因用户程序继续运作而导致标记产生变动的那一部分标记记录,虚拟机将这段时间对象变化记录在线程Remembered Set Logs里面,最终标记阶段需要把Remembered Set Logs的数据合并到Remembered Set中,此阶段是stop-the-world操作,使用snapshot-at-the-beginning (SATB) 算法。


5.筛选回收(Live Data Counting and Evacuation):首先对各个Region的回收价值和成本进行排序,根据用户所期望的GC停顿时间来制定回收计划,回收没有存活对象的Region并加入可用Region队列。这个阶段也可以做到与用户程序一起并发执行,但是因为只回收一部分Region,时间是用户可控制的,而且停顿用户线程将大幅提高收集效率

与其它收集器相比,G1变化较大的是它将整个Java堆划分为多个大小相等的独立区域(Region),虽然还保留了新生代和老年代的概念,但新生代和老年代不再是物理隔离的了它们都是一部分Region(不需要连续)的集合。同时,为了避免全堆扫描,G1使用了Remembered Set来管理相关的对象引用信息。当进行内存回收时,在GC根节点的枚举范围中加入Remembered Set即可保证不对全堆扫描也不会有遗漏了。

特点:

1、并行与并发:G1能充分利用CPU、多核环境下的硬件优势,使用多个CPU(CPU或者CPU核心)来缩短stop-The-World停顿时间。部分其他收集器原本需要停顿Java线程执行的GC动作,G1收集器仍然可以通过并发的方式让java程序继续执行。

2、分代收集:分代概念在G1中依然得以保留。虽然G1可以不需要其它收集器配合就能独立管理整个GC堆,但它能够采用不同的方式去处理新创建的对象和已经存活了一段时间、熬过多次GC的旧对象以获取更好的收集效果。也就是说G1可以自己管理新生代和老年代了。

3、空间整合:由于G1使用了独立区域(Region)概念,G1从整体来看是基于“标记-整理”算法实现收集,从局部(两个Region)上来看是基于“复制”算法实现的,但无论如何,这两种算法都意味着G1运作期间不会产生内存空间碎片。

4、可预测的停顿:它可以自定义停顿时间模型,可以指定一段时间内消耗在垃圾回收商的时间不大于预期设定值。

使用场景

G1 GC切分堆内存为多个区间(Region),从而避免很多GC操作在整个Java堆或者整个年轻代进行。G1 GC只关注你有没有存货对象,都会被回收并放入可用的Region队列。G1 GC是基于Region的GC,适用于大内存机器。即使内存很大,Region扫描,性能还是很高的。

Remembered Set

我们之前说过,G1在回收每个Region上的垃圾时,每个Region之间又有相互依赖引用关系,想要做到对全部Region进行扫描清理,那么不得不做一次全堆扫描。这样就降低了垃圾回收的效率。所以HotSpot引入了Remembered Set来专门存储于管理对象的引用依赖关系,这样当每次回收时,只需要根据Remembered Set上面的对应关系找到相对的区域进行清理,这样就可以避免扫描整个堆内存又不会遗漏某一个区域。

OopMap

我们都知道在GC之前要做一次GC Roots来查找对象的存活情况,一边在GC时候正确的回收。那么每次GC时候遍历所有的引用是不现实的,那么这之后就引入了OopMap,它里面记录了一些类加载时候的类型与偏移量地址等信息生成一张映射表放在OopMap中。GC开始的时候,就通过OopMap这样的一个映射表知道,在对象内的什么偏移量上是什么类型的数据,而且特定的位置记录下栈和寄存器中哪些位置是引用。

安全点/安全区域(Safepoint/Safe Region)

上面为了快速的分析可达性,使用了一个引用类型映射表,可以快速的知道对象内或者栈和寄存器中哪些位置引用了。那么在方法执行过程中,这些引用关系可能会随时发生变化,那么OopMap是不是也要跟着变呢?如果没出引用变化就更新OopMap那么也是不现实的,这时候就引入了安全点的概念。OopMap的作用就是在每次GC前保证是最新的就可以了。OopMap只需要在预先选定的一些位置上记录变化的OopMap就行了。在这个状态下虚拟机堆栈不在发生变化。而安全点的选定是以程序‘是否具有让程序长时间执行的特征’为标准选定的。‘长时间执行’的明显特征就是指令序列复用,例如:方法调用(方法临返回前/调用方法的call指令后),循环跳转(循环的末尾),异常跳转(可能抛异常的位置)等,具有这些功能的指令才再回产生安全点。大白话就是在程序中寻找一个安全点,当GC触发时,为了线程状态和数据的一致性,让线程都跑到这个安全点停顿下来后再执行GC。至于安全区域你可以认为在这个区域的任何位置都可以GC,即点.线,面的关系。基于安全点中断GC的方式有两种:

  1. 抢先式中断(Preemptive Suspension):抢先式中断不需要线程的执行代码主动去配合,在GC发生时,首先把所有线程全部中断,如果发现有线程中断的地方不在安全点上,就恢复线程,让它“跑”到安全点上。现在几乎没有虚拟机采用这种方式来暂停线程从而响应GC事件。
  2. 主动式中断(Voluntary Suspension):主动式中断的思想是当GC需要中断线程的时候,不直接对线程操作,仅仅简单地设置一个标志,各个线程执行时主动去轮询这个标志,发现中断标志为真时就自己中断挂起。轮询标志的地方和安全点是重合的,另外再加上创建对象需要分配内存的地方。

卡表(Card Table)

有个场景,老年代的对象可能引用新生代的对象,由于新生代的垃圾收集通常很频繁,那标记存活对象的时候,需要扫描从老年代到新生代的所有引用对象。因为该对象拥有对新生代对象的引用,那么这个引用也会被称为GC Roots。那不是每次YGC时又得做全堆扫描?显然不是,对于HotSpot JVM,使用了卡标记(Card Marking)技术来解决老年代到新生代的引用问题。具体是,使用卡表(Card Table)和写屏障(Write Barrier)来进行标记并加快对GC Roots的扫描。卡表的设计师将堆内存平均分成2的N次方大小(默认512字节)个卡,并且维护一个卡表,用来储存每个卡的标识位。当对一个对象引用进行写操作时(对象引用改变),写屏障逻辑将会标记对象所在的卡页为脏页。在YGC只需要扫描卡表中的脏卡,将脏中的对象加入到YGC的GC Roots里面。当完成所有脏卡扫描时候,虚拟机会将卡表的脏卡标志位清空。

在高并发环境下,每次对引用的更新,无论是否更新了老年代对新生代对象的引用,
都会进行一次写屏障操作,频繁的写屏障很容易发生虚共享(false sharing),从而带来性能开销。

举个例子:

假设CPU缓存行大小为64字节,由于一个卡表项占1个字节,这意味着,64个卡表项将共享同一个缓存行。
HotSpot每个卡页为512字节,那么一个缓存行将对应64个卡页一共 64*512=32KB。
如果不同线程对对象引用的更新操作,恰好位于同一个32KB区域内,这将导致同时更新卡表的同一个缓存行,
从而造成缓存行的写回、无效化或者同步操作,间接影响程序 性能。

在JDK 7中引入了VM参数-XX:+UseCondCardMark ,意思就是现在不采用无条件写屏障,而是先检查此卡是否已经是脏页,如果是将不再标记。这样就减少了并发下的虚共享问题。但是这样却不能避免对未标记的页进行并发标记。

G1 GC和CMS GC相比的优缺点?

G1 GC 这是一种兼顾吞吐量和停顿时间的 GC 实现,是 Oracle JDK 9 以后的默认 GC 选
项。G1 可以直观的设定停顿时间的目标,相比于 CMS GC,G1 未必能做到 CMS 在最好情
况下的延时停顿,但是最差情况要好很多。


G1 GC 仍然存在着年代的概念,但是其内存结构并不是简单的条带式划分,而是类似棋盘的
一个个 region。Region 之间是复制算法但整体上实际可看作是标记 - 整理(Mark-
Compact)算法,可以有效地避免内存碎片,尤其是当 Java 堆非常大的时候,G1 的优势更
加明显。

Parrallel GC,(jdk8默认GC)在早期 JDK 8 等版本中,它是 server 模式 JVM 的默认 GC 选择,也被称作是吞吐量优先的 GC。

参考链接:一篇文章彻底搞懂CMS与G1 - 知乎

参考链接:CMS和G1的区别,以及Parallel - 不死码农 - 博客园

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值