个人博客请访问 http://www.x0100.top
G1 GC,全称Garbage-FirstGarbage Collector,通过-XX:+UseG1GC参数来启用,作为体验版随着JDK 6u14版本面世,在JDK 7u4版本发行时被正式推出,相信熟悉JVM的同学们都不会对它感到陌生。在JDK 9中,G1被提议设置为默认垃圾收集器(JEP 248)。那么与之前的CMS相比,G1有哪些改变,哪些优势呢?
CMS收集器是基于标记清除算法的一种并发的,低停顿的收集器,值得注意的一点是,CMS只是低停顿而不是没有停顿
CMS分为以下四步:
l 初始标记
l 并发标记
l 重新标记
l 并发清除
初始标记和重新标记都是需要Stop the world的。
初始标记仅仅是记录CG root关联的对象,因此停顿时间比较短,并发标记是进行GC RootTracing,重新标记是修正并发标记期间标记的改动(时间比初始标记稍长一点),之后就是进行并发清除, 由于最耗时间的工作都是在并发操作中完成的,所以CMS的停顿会比较低
下面是CMS的过程图
CMS是一个优秀的垃圾回收器,但是他也有不少缺点:
l 他没有办法处理浮动垃圾(就是在初始标记之后产生的垃圾)
l 他会占用大量的CPU资源
l 他会产生碎片空间(当发生FullGC时会使用Serial Old回收器来处理碎片空间)
l 他在回收垃圾时,由于是并行的收集,所以需要的空间比较大
G1是一个并行回收器,它把堆内存分割为很多不相关的区间,每个区间可以属于老年代或者年轻代,并且每个年龄代区间可以是物理上不连续的。老年代区间这个设计理念本身是为了服务于并行后台线程,这些线程的主要工作是寻找未被引用的对象,而这样就会产生一种现象,即某些区间的垃圾(未被引用对象)多于其它的区间。垃圾回收时都是需要停下应用程序的,不然就没办法防止应用程序的干扰。G1 GC可以集中精力在垃圾最多的区间上,并且只费一点点时间就可以清空这些区间的垃圾,腾出完全空闲的区间。由于这种方式的侧重点在于处理垃圾最多的区间,所以我们给G1一个名字:垃圾优先(Garbage First)。
G1内部有四个操作阶段:
l 年轻代回收;(AYoung Collection)
l 运行在后台的并行循环;(ABackground,Concurrent Cycle)
l 混合回收;(A MixedCollection)
l 全量回收;(A FullGC)
分代收集
这个现在是垃圾回收器的标配,G1和CMS也不例外。但是G1同时回收老年代和年轻代,而CMS只能回收老年代,需要配合一个年轻代收集器。另外G1的分代更多是逻辑上的概念,G1将内存分成多个等大小的region,Eden/ Survivor/Old分别是一部分region的逻辑集合,物理上内存地址并不连续。
CMS在old gc的时候会回收整个Old区,对G1来说没有old gc的概念,而是区分Fullyyoung gc和Mixed gc,前者对应年轻代的垃圾回收,后者混合了年轻代和部分老年代的收集,因此每次收集肯定会回收年轻代,老年代根据内存情况可以不回收或者回收部分或者全部(这种情况应该是可能出现)。
如何处理跨代引用
在垃圾回收的时候都是从Root开始搜索,这会先经过年轻代再到老年代,对于年轻代引用老年代的这种跨代不需要单独处理。但是老年代引用年轻代的会影响young gc,这种跨代需要处理。
为了避免在回收年轻代的时候扫描整个老年代,需要记录老年代对年轻代的引用,young gc的时候只要扫描这个记录。CMS和G1都用到了Card Table,但是用法不太一样。JVM将内存分成一个个固定大小的card,然后有一个专门的数据结构(即这里的Card Table)维护每个Card的状态,一个字节对应一个Card,有点像内存page的概念,只是page是硬件上的,Card Table是软件上的。当一个Card上的对象的引用发生变化的时候,就将这个Card对应的Card Table上的状态置为dirty,young gc的时候扫描状态是dirty的Card即可。这是基本的用法,CMS基本上就是这么使用。
G1在Card Table的基础上引入的rememberedset(下面简称RSet)。每个region都会维护一个RSet,记录着引用到本region中的对象的其他region的Card。比如A对象在regionA,B对象在regionB,且B.f = A,则在regionA的RSet中需要记录B所在的Card的地址。这样的好处是可以对region进行单独回收,这要求RSet不只是维护老年代到年轻代的引用,也要维护这老年代到老年代的引用,对于跨代引用的每次只要扫描这个region的RSet上的Card即可。
上面说过年轻代到老年代的引用不需要单独处理,这带来了很大的性能上的提升,因为年轻代的对象引用变化很大,如果都需要记录下来成本会很高。同时也说明只需要在老年代维护Card Table。
如何处理并发过程的对象变化
CMS和G1都有并发处理过程,这个过程应用程序跟着gc线程一起运行,会产生新对象,也会有旧的对象死去,对象之间的引用关系也会发生变化。这部分数据可以暂时不处理,留到下一次再处理吗?如果可以这样的话问题就会变得很简单,但是答案是不行。考虑下图的场景(图中每一行表示一个内存状态,每一列表示一个Card,这里有4个):第一步a是并发标记中途的一个状态,标记了a b c e四个对象,0 1两个Card已经标记好;第二步b并发标记的同时引用发生变化,g不再指向d,而b不再指向c,变成指向d,这个时候处理Card 2,会标记到g,然后就标记结束了,导致d对象丢失。
CMS初始标记的时候会标记所有从root直接可达的对象,并发标记的时候再从这些对象进一步搜索其他可达对象,最终构成一个存活的对象图。并发标记过程中引用发生变化的也是通过Card Table来记录。但是young gc的时候如果一个dirty card没有包含到年轻代的引用,这个card会重新标记为clean,这有可能将并发标记过程产生的dirty card错误清除,因此CMS引入了另一个数据结构mod union table,这里一个bit对应一个Card,young gc在将Card Table设置为clean的时候会将对应的mod union table置为dirty。最终标记的时候会将Card Table或者mod union table是dirty的Card也作为root去扫描,从而解决并发标记过程产生的引用变化。CMS还需要处理并发过程从年轻代晋升到老年代的对象,处理方式是将这部分对象也作为root去扫描。
G1使用一个称为snapshot at thebeginning(下面简称SATB)的算法,在初始标记的时候得到一个从root直接可达的snapshot,之后从这个snapshot不可达的对象都是可以回收的垃圾,并发过程产生的对象都默认是活的对象,留到下一次再处理。对于引用关系发生变化的,将这个对象对应的Card放到一个SATB队列里,在最终标记的时候进行处理(如果超过一定的阈值并发标记的时候也会处理一部分),处理的过程就是以队列中的Card作为root进行扫描。
Write Barrier
Write Barrier可以理解为在写的时候插入一条特定的操作。
在CMS中老年代引用年轻代的时候就是通过触发一个Write Barrier来更新Card Table的标志位。这是一个同步操作,在更新引用的时候顺带执行,只需要两个指令,引入的消耗不大。
G1比较复杂,在两个地方用到了WriteBarrier,分别是更新RSet的rememberd set Write Barrier和记录引用变化的ConcurrentMarking Write Barrier,前者发生在引用更新之后,称为Post Write Barrier,后者发生在引用变化之前,称为Pre Write Barrier。G1为了提高性能,这两个Write Barrier都是先放到队列中,再异步进行处理。
Full GC
导致CMS Full GC的可能原因主要有两个:Promotion Failure和Concurrent Mode Failure,前者是在年轻代晋升的时候老年代没有足够的连续空间容纳,很有可能是内存碎片导致的;后者是在并发过程中jvm觉得在并发过程结束前堆就会满了,需要提前触发Full GC。CMS的Full GC是一个多线程STW的Mark-Compact过程,,需要尽量避免或者降低频率。
G1的初衷就是要避免Full GC的出现,Full GC会会对所有region做Evacuation-Compact,而且是单线程的STW,非常耗时间。导致G1Full GC的原因可能有两个:1. Evacuation的时候没有足够的to-space来存放晋升的对象;2. 并发处理过程完成之前空间耗尽。这两个原因跟CMS类似。
关注微信公众号和今日头条,精彩文章持续更新中。。。。。