随着垃圾收集器不断的完善,中间产生了多种垃圾收集器,从最早的Serial收集器到现在的ZGC,Shenandoah GC收集器。这些垃圾收集器其最终的目的是为了能够缩短STW(Stop the World)时间,减少系统卡顿,提升系统性能。虽然ZGC,Shenandoah GC已经诞生,但是仍处于测试阶段,并未适用在生产环境。当前在HotSpot虚拟机下使用较多的仍然是CMS,G1等。
CMS
CMS是作用在老年代的垃圾收集器,支持与用户线程并发工作,可以结合ParNew收集器与Serial收集器协同工作。可以使用参数-XX:+UseConcMarkSweepGC
来使用CMS。CMS垃圾收集器的工作流程可以分为初始标记,并发标记,重新标记,并发清除,并发重置这五个部分。其中在初始标记和重新标记阶段会触发STW。
stop the world(STW):停止用户线程。
收集过程
初始标记
初始标记过程中,仅仅标记GC Roots能够直接关联的对象和一些年轻代引用老年代的对象,这个过程很快并且会触发 STW。如果不触发STW,那么可能就会造成对应引用的不断变更或者不断生成新的垃圾对象。-XX:+CMSParallelInitialMarkEnabled
参数可以开启初始标记以多线程的方式进行 ,从而提高标记效率,缩短STW的时间。
并发标记
并发标记过程中允许用户线程正常执行,采用三色标记算法
从初始标记的对象开始遍历整个老年代,进行存活对象标记,这个过程相对过长,但对于用户来说是几乎无感知的。
在并发标记的过程中,用户线程也在同时执行,所以老年代中也有可能在随时发生变化。比如有新的对象进入了老年代,年轻代引用了老年代中的对象或者老年代引用了年轻代中的对象这些跨代引用现象。为了解决这种问题,Hotspot虚拟机采用的卡表(Card Table)
的方式记录跨代引用的对象。
重新标记
由于在并发标记过程中是与用户线程并发执行的,会存在一些对象引用关系的变更或者是一些新对象进入到了老年代,所以会产生一些漏标或者错标的对象。这个时候就需要再次停止用户线程,进行重新标记。在重新标记阶段,会遍历整个年轻代和老年代进行存活对象标记。遍历年轻代的原因是因为可能存在跨代引用的对象。这个过程是比较耗时的,所以我们可以通过开启-XX:+CMSScavengeBeforeRemark
参数,这个参数的作用是在执行重新标记前,先进行一次MinorGC,将垃圾对象进行清除,这样的话只需要扫描幸存区就可以了,而且遍历扫描的时间也会减少一些。
并发清除
经过上面几次标记后,就可以对垃圾对象进行清理了。此时因为用户线程也在运行,所以会有新的垃圾对象生成,这些对象称为浮动垃圾
,只能等待下次垃圾回收时才能进行清理。
三色标记
三色标记是CMS采用的标记对象算法,可以缩短STW时间并达到标记存活对象的效果。按照对象是否被垃圾收集器访问过这个条件,标记的颜色有下面三种:
- 白色 :表示该对象还没有被访问过。在可达性分析开始阶段,除GC Root对象外,所有的对象节点都是白色的。如果在可达性分析执行完后,还有白色状态的对象,即对象不可达,那么这些就可以被认定为垃圾对象。
-
黑色 :表示该对象已经被访问过,并且该对象的引用对象也全部被访问过,该对象可达,为存活对象。当遍历后,则确定对象F,G,H为垃圾对象。
-
灰色 :表示该对象已经被访问过,但是存在引用对象还没有被访问过。比如在访问A对象时,A对象内部引用了B和C对象,当访问B对象时,发现内部没有引用其他对象,那么此时B对象已经被GC访问过了。但对于A对象来说,尽管B对象已经被访问,C对象还没有被访问,所以A的对象标记为灰色。
上面部分中有提到,在并发标记过程中不会触发STW,会导致对象引用关系的变更。那么就会产生一些问题。比如A对象被垃圾回收器访问后被标记成了黑色,但是随用用户线程的执行A对象和H对象产生了引用关系,但是A已经被标记为黑色了,不会在被重新访问,那就意味着H对象被误认为垃圾对象了,当H对现象被回收后,那将会有严重的错标
问题了。
除错标问题外,还有另外一种情况。当扫描到C对象时,由于C对象还有引用对象没有被扫描,此时C对象会被标为灰色。但是由于用户线程的运行,A对象和C对象的引用关系被取消,垃圾收集器会继续由C对象向下进行可达性分析。原则上C,D,E对象将会是垃圾对象,但是实际上这些对象仍然会被标记为存活对象,这种情况称为漏标
。
如果出现漏标情况,那么会产生一些浮动垃圾,等待下次GC时会被清理。如果出现错标,将存活的对象给清理掉了,这可能会导致严重的问题。针对这些问题,有两种解决方案:增量更新和原始快照(SATB)
。增量更新适用于黑色对象引用白色对象时,会将这个引用关系记录下来,等扫描结束后,以黑色对象为根节点进行二次遍历扫描。原始快照适用于当灰色对象指向白色对象的引用被断开时,就将这条引用关系记录下来。当扫描结束后,再以这些灰色对象为根,重新扫描一次。在CMS中采用的是增量更新,而在G1中采用的SATB。
记忆集与卡表
上面有提到跨代引用的问题,如果年轻代中的对象被老年代中的对象引用,那么当发生Minor GC时,为了确保所有存活对象不被清除,那么不得不在固定的GC Roots之外,再额外遍历整个老年代中所有对象来确保可达性分析结果的正确性,那这个操作将会花费一定的时间,效率不高。为了解决这个问题,在年轻代区域引入了 记忆集(Remembered Set) 。记忆集是一种用于记录从非收集区域指向收集区域的指针集合的抽象数据结构,这种结构把老年代的内存区域划分为一块一块的子内存区域,这些子内存区域称为 卡页(Card Page) ,然后会标记出哪一些子内存区域会存在跨代引用,当发生Minor GC时,只需要扫描被标记的子内存块即可,避免了扫描了整个老年代内存区域。 卡表 (Card Table)就是记忆集的一种具体实现(可以理解为接口和实现类的关系)。如果老年中的对象引用年轻代中的对象,那么该对象所在的Card Page将会标记为Dirty,并在Card Table维护一个标识(1),表示该卡页存在跨代引用对象。
CMS存在的问题
-
容易产生内存碎片
由于CMS采用的是"标记-清除"算法,那么在运行到一定时间后,会产生一些内存碎片。当有新的对象要进入老年代时,可能会造成内存不够分配的情况。这个时候可以通过参数
-XX:CMSFullGCsBeforeCompaction
进行内存整理。比如配置-XX:CMSFullGCsBeforeCompaction=5
,那么每执行5次Full GC就会对老年代进行内存空间整理。
-
产生浮动垃圾
在并发清除过程中,由于用户线程也在不断的运行,所以会产生一些垃圾对象,这些垃圾对象叫做浮动垃圾。这些垃圾对象只能等待下次GC时才能被回收,在没被回收之前仍然占用内存空间。
-
占用系统资源
由于CMS支持与用户线程并行,所以会与用户线程进行CPU资源争夺。
-
Concurrent Mode Failure
有没有想过这样一个问题,当CMS正在执行并发清理时(还没有清理完),年轻代触发MinorGC并且年轻代的部分对象晋升到了老年代。但是此时老年代正在执行并发清理工作,空间已经不足接受新的对象了。这种现象称为
Concurrent Mode Failure
,一旦出现这种情况垃圾收集器将会由CMS切换到Serial Old,从而暂停用户线程且停顿时间较长。这个时候可以使用参数-XX:CMSInitiatingOccupancyFraction=n
来调整GC触发频率并且预留部分内存空间,设置之后当内存使用达到n%后,会触发GC。
G1
在JDK9后,默认的垃圾收集器为G1,CMS垃圾收集器仍然可以被使用,但是已经不推荐使用了,在未来的版本中CMS有可能被弃用。G1垃圾收集器将堆内存划分为多个大小相等的独立内存区域,这种单独的内存区域称为 Region 。这些Region块,可以是Eden区域,可以是Old区域,也可以是Survivor幸存区或Humongous区(用于存储大对象,当对象的大小超过Region大小的50%时,该对象为大对象)。与CMS内存布局不同的是,G1的老年代,年轻代都不需要连续的内存空间。
G1中整个堆空间最多可以划分为2048个Region区域,假如对空间的大小为2G,那每个Region的区域大小就是1MB左右。如果需要的话,也可以通过参数-XX:G1HeapRegionSize=n
来设置Region大小(1<= n <=32)。
收集过程
G1收集器的回收过程大体上与CMS回收过程类似,分为初始标记,并发标记,最终标记,筛选回收四个步骤,其中在初始标记阶段,最终标记阶段以及筛选回收阶段会触发STW。
- 初始标记 :与CMS过程中的初始标记一样,仅标记GC Root直接关联的对象。
- 并发标记 :与CMS过程中的并发标记一样,从初始标记的对象开始向下遍历,标记存活对象,允许与用户线程同时进行。
- 最终标记 :与CMS过程中的重新标记一样,标记引用关系变更或区域变更的对象。
- 筛选回收 :该阶段会根据每个Region区域的回收价值高低并结合用户所期望的GC停顿STW时间(可以通过参数
-XX:MaxGCPauseMillis
设置,默认200ms)来制定回收计划来并进行回收。如果G1发现回收时间要大于200ms,那么将会筛选部分Region进行局部回收。比如Region01区域内有2M垃圾对象,回收时间需要1ms,Region02区域内有1M垃圾对象,回收时间需要2ms。那么这种情况下Region01的回收价值要高于Region02。
G1垃圾回收分类
为了避免与CMS的垃圾回收分类混淆,这里需要特别解释一下,有些地方还是不同的。
-
Yong GC
G1的YongGC并不会像CMS那样等到年轻代的Eden空间满了在触发GC,而是与用户设置的
-XX:MaxGCPauseMillis
有关。G1会计算YongGC的回收时间,如果回收时间小于MaxGCPauseMillis的值,那么会继续增加Eden的Region区域用于存放新对象,直到回收时间接近MaxGCPauseMillis值时,才会进行YongGC。 -
Mixed GC
MixedGC发生在,当老年代Region的空间比率达到参数
-XX:InitiatingHeapOccupancyPercent=n
设定的值时。回收所有的Eden Region,部分Old Region以及Humongous Region。 -
Full GC
在MixedGC时,会将存活的对象复制到新的Region中。如果所有的Region都已经满了,没有足够的空间存储对象了,那么这个时候会触发FullGC。G1的FullGC采用单线程的方式进行标记压缩,效率不高。
总结
上面对常用的两款CMS与G1垃圾收集器做了简单的介绍,了解了各收集器的优点和缺点以及存在的问题。CMS适用于小容量内存,容易产生浮动垃圾和内存碎片。G1收集器将堆内存Region化,适用于大容量内存,具有可预测的停顿时间模型。