CMS和G1垃圾回收器

Serial收集器:是一个单线程的年轻代收集器,当它运行进行垃圾回收时,其它工作线程必须被停止,直到它收集完成。serial依然是JVM client模式下的默认收集器,对于client模式是一个很好的选择。

parNew收集器:它也是一个年轻代收集器,parNew收集器是serial收集器的多线程模式,除了使用多个线程进行垃圾回收外,其余与serial基本完全相同,preNew是jvm server模式下的默认收集器。

serial和parNew收集器是唯一能与CMS配合使用的收集器。

下面主要介绍一下CMS垃圾回收器(java8老年代默认使用的就是CMS)和G1垃圾回收器,在这之前先简单的记录两点 GC roots的选取和三色标记法。

首先需要指出的是gc root是一组引用,那么是从哪些引用中选取呢?栈中活跃栈帧里指向堆中对象的引用,方法区中指向静态对象的引用及方法区中指向常量对象的引用。

我们知道可达性分析法,就是选取gc roots,然后从每一个gc root开始,寻找它所直接引用的对象,然后从这些直接引用对象一直向下遍历寻找间接引用的对象,直到一个对象中没有引用其他任何对象。

而三色标记是将对象分为黑色、灰色和白色,黑色对象是指该对象和它直接引用的对象都已经被扫描过,灰色对象是该对象已经被扫描过,但其引用的对象还没有被扫描完,白色是指那些没有被扫描过的对象,垃圾回收阶段就是利用可达性分析法,对对象进行遍历上色,最终那些白色对象都会被回收掉。

CMS(Concurrent Mark Sweep):使用标记清除法的多线程、并发收集器,作用于老年代垃圾收集器。在垃圾回收时提供与客户线程并发执行的能力。比较适合用于为用户提供良好体验的服务上,比如web服务等注重响应速度,停顿时间需要短的项目中,JVM的内存也要大一些。

它的执行过程包括:初始标记,仅仅将与GCRoots直接关联的对象进行标记,速度很快;并发标记,用户程序可以与该步骤同时运行,从GCRoots直接关联的对象开始,寻找所有可达的对象;重新标记,由于在第二步进行标记时用户程序是在运行的,所以可能会对标记进行影响并产生新的垃圾,重新标记就是进行过滤,标识出存活的对象,这一步是多线程进行标记的;并发回收,与用户线程并发运行,对垃圾对象进行回收

(1)初始标记:会暂停用户程序,利用gc root找出那些直接引用的老年代对象,并且会遍历新生代区域找到新生代对象引用的老年代对象(这部分老年代对象会比较少,但也有),找到后是将它们置为灰色,因为它引用的对象并没有被扫描

(2)并发标记:利用可达性分析及三色标记法从初始标记阶段发现的对象开始向下遍历,并将对象上色,在这一步中,垃圾回收线程是与用户线程并发运行的。

但是由于在这一步中垃圾回收线程是与用户线程并发运行的,用户线程有可能会对老年代中的对象引用做出一些改变,或者有一些大对象直接分配在了老年代,或年轻代进行垃圾回收后出现了对象的晋升。改变老年代的引用主要就是将一个被标记为黑色的对象指向了一个白色对象,但是该白色对象失去了所有直接或间接指向自己的灰色对象引用,那么该白色对象就不能被正常扫描,如果直接回收的话,就会出现误回收。

下面说一下,在cms中是怎么处理这种情况的,cms中存在一个card table可以简单理解为是一个字节数组,数组中每一项为一个字节,并且将老年代划分为多个大小相同的card区域,每一个区域对应于card table中的一个节点;还使用了内存写屏障,在cms中称为Incremental Update,当引用发生改变时,如果某对象新增了引用,那么就该对象所在card区域置为dirty,即在card table对应的字节改变某一位(1个字节8位)的数值,如A->C是一条新加引用,那么就会将A所在card置为dirty。并且在这一步还会记录用户的操作log。

(3)重新标记:在这一步会暂停用户线程的。在这一步会处理用户线程对老年代引用的改变,结合用户的操作log重新从gc root及年轻代进行扫描,并且会扫描被标记为dirty的card区域,防止有存活对象被遗漏。大家可能会疑惑,是不是会漏标一些对象,比如像新晋升或刚创建的大对象,其实是不会的,因为它还会从gc root和年轻代用重新寻找一次直接引用的对象,然后再进行扫描,这些新对象肯定会被发现,同时因为有许多对象已经被标记过了,所已速度并不会很慢。

(4)并发回收:在并发回收阶段,会将那些未被标记的白色对象进行回收,这一步用户线程并发运行。

CMS的缺点:(1)对CUP资源比较敏感,因为它可以与用户线程并发运行,会占用一定的CPU资源,对用户线程产生一些影响,如果是单核CPU可能影响较大

(2)会产生浮动垃圾,因为并发标记时会有用户线程在运行,此时有可能会修改老年代对象的引用关系,虽然我们使用了Incremental Update和card table的方式防止有对象被遗漏标记,但是也有一些原本被标记为黑色的对象,它的指向它的引用被用户线程修改,导致再也没有引用指向它,但由于被标记导致本次并没有被正常回收,只能等待下一次再回收

(3)如果新对象分配时空间不足也会报异常引发新的Full GC,老年代使用的是serial old年轻代是serial,这就解释了为什么第一个图cms关联了年轻代中的par new还要关联serial,因此不能等到老年代被装满时再进行垃圾回收,所以它适用于JVM内存较大的情况,默认是老年代使用率到达92%就发生了

(4)因为它是标记清除法所以会产生内存碎片,需要定期进行整理。

G1收集器(Garbage First):它是一个整堆收集器,分为young gc和mix gc,mix gc中既包含年轻代垃圾回收又包含部分老年代垃圾回收。利用多CPU、多核实现了多条垃圾回收线程的并行运行,并且支持用户程序并发运行。在整体上采用的是标记整理法不会再产生大量内存碎片,局部使用的是复制法。

G1将堆中的区域分成一个个Region,每个Region拥有自己的分代属性,在物理上G1并不要求同一类的region内存连续,只要求逻辑上连续,G1是以region为垃圾回收的基本单位。humongous region表示大对象区域,一个对象的大小如果超过了region最大限制区域的一半以上,就会将它放到H region之中,若它超过了单个region的大小,则会为它申请多个连续的h region进行存储,因为巨型对象的复制移动成本很高,避免放到普通region经常进行gc,造成频繁的复制移动,灰色表示还没有被分配的区域。对于一个region来说它的空间分为两部分,已分配的和未分配,它们之间的界限成为top,当新对象被分配时,仅仅是移动top的值。

G1提供更加可控的gc停顿时间,可以通过设定MaxGCPauseMillis=N ms标明垃圾收集期望停顿时间,表明垃圾回收最多不超过N ms。InitiatingHeapOccupancyPercent=45,表示当堆使用率到达45%执行mix gc。并且我们可以根据 -XX:G1HeapRegionSize=n可指定分区大小(1-32M),默认会划分为2048个分区。实际的分区数量需要根据分区大小去确定,region小数量就多。

补充一些额外的小知识,G1垃圾回收概念是在2004年提出的,在java 7中正式支持,在java 9中成为默认的垃圾回收器(应该是,因为没有公司项目和我自己本地都是用的8,大家装了9的可以告诉我一下,到底是不是)

此外对每一个region来说,G1将每一个region划分为多个card区域,每一个region又对应一个rememer set(rset),region的每一个card都对应rset中的一块区域,在rset中记录了其他region 到本region的某card引用,即谁引用了我,示例图如下

G1的年轻代垃圾回收:

年轻代的垃圾回收过程与传统的分代收集相似,某eden区满了之后,使用复制算法将存活对象复制到survivor区域然后清空,某survivor区域满了之后也会进行存活对象的复制,将其存储到其他的survivor区域,当一个对象的年龄到达界限之后就会转移至old region之中。在这个过程中就使用了rset中存储的信息,避免了扫描整个老年代,可以很快速的找出引用了本region的老年代对象。

G1 mix gc的执行过程

(1)初始标记:标记与GCRoots直接相连old region的对象,并且该阶段会伴随年轻代的垃圾回收,该过程中会出现STW(stop the world),用户其它线程会被暂停

(2)根区间扫描:因为有一部分老年代region 对象会被年轻代引用,但是由于年轻代垃圾回收较为频繁,rset中不存储从young region出发的引用,经过了第一步中的垃圾回收后,eden中的存活对象会被移动到了surivivor region,在这一步会扫描survivor中存活对象,找到其引用的老年代对象。

(3)并发标记:从第(1)(2)步找到的老年代对象开始,进行可达性分析,找出存活的对象,使用三色标记法进行标记,在该过程中用户线程可以并发执行。在这一步和cms存在同样的问题,由于用户线程的并发运行,可能会修改对象间的引用关系,造成黑色对象引用了失去所有直接或间接灰色对象引用的白色对象,还有就是在初始标记和并发标记过程中都可能发生年轻代对象的晋升,那么G1是如何处理的呢?

对于黑色对象引用的白对象未被正常标记的情况,G1使用了SATB(Snapshot at the begin)标记前快照,g1会在标记阶段开始前存下当前堆中对象关系的快照,认为快照中存活的对象都是不可回收的,它也是使用了内存写屏障write barrier(类似于引用赋值关系的一种环切,如原本a->b,现在变成了c->b),那么对于cms而言,它使用的是post,新引用关系的建立,而G1使用的pre,前置关系的断开,当发生引用关系断开的时候,会将原引用存储进write barrier log之中,用于后续扫描。

而对于新对象在old region中的分配,是使用了TAMS(top-at-mark-start) ,因为我们前面已经说了,region是由top标识已分配空间和未分配空间的,在这里使用了tams-pre和tams-next两个指针,它们指向了标记阶段开始后,新分配对象占有的内存空间。它们两个之间的即为新分配的对象,这一部分对象被G1认为是已隐式标记的,不会被回收掉。

(4)最终标记:参考write barrier log等数据,对old region进行重新标记,选出不可被回收对象,这一过程中用户线程被停止。

(5)筛选回收:分析各个region回收的价值和所需代价,在满足用户设定回收停顿时间的条件下筛选需要被回收的region,此过程用户线程被停止,之后进行存活对象复制及region回收该过程用户线程可以并发运行

CMS与G1的不同:

(1)CMS使用标记清除法,G1采用复制法,G1不会产生内存碎片

(2)G1提供更为可控的垃圾回收时间,可以根据项目的需要进行设置

(3)CMS和G1处理跨代引用的方式不同(主要是老年代引用年轻带):

在CMS中JVM的老年代会维护一个card table,堆内存会将内存空间分为一个个大小相同的卡页(card),card table用于标记每个card区域的对象引用了哪些对象。对于老年代来说,如果它的一个对象引用了年轻代的对象,那么该老年代对象所在card对应的card table的位置状态就会变成dirty,那么在进行年轻代垃圾回收时,就不用扫描整个老年代,只需要扫描老年代的card table状态为dirty的区域,就可以找到被引用的年轻代对象(即年轻代存活的对象),减少了GC的时间。这里的标为dirty和前面说的cms并发标记过程中引用新建时将对应card标为dirty应该是其对应card table字节数组字节的不同位。

而在G1垃圾回收的时候,在card table的基础上又使用了Remember set(Rset),对于G1来说,每一个region都有一个Rset,Rset用于记录有哪些其它region的对象引用了本region的对象,记录其它region引用对象所在card的地址,假如regionA有RsetA和对象A,regionB有RsetB和对象B,B对象引用了对象A,则regionA的RsetA记录key regionB,value是B对象所在card的地址。这有利于对单个region进行回收,避免扫描整个堆寻找对本region对象的引用。对于G1来说,它分为年轻代回收和混合回收(mix gc),年轻代回收即为edan区满了(那一套),此时可以通过region的Rset来寻找老年代对象对年轻代对象的引用;而混合回收包括年轻代回收和老年代回收,在老年代回收中可以利用老年代的region的Rset寻找被其他老年代引用的对象,从而避免对整个堆内存的扫描,大大减少了GC时间。(年轻代的引用不会保存,如年轻代引用年轻代,年轻带引用老年带,因为年轻带变化的太快了,记录下来消耗会很大)

(4)回收的在并发标记阶段,对用户线程改变对象间引用,造成不可回收对象未被正确标记的处理方式不同。虽然它们都使用了内存写屏障,但sms使用的是increamental update ,post新引用的创建,g1是satb ,pre 原引用的断开

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值