全面理解CMS和G1垃圾回收器

899 阅读7分钟

前置知识

1.分代回收

年轻代

一般用的是复制算法 对应的是Minor GC image.png

如图所示,年轻代分为:一个伊甸园空间(Eden ),两个幸存者空间(Survivor )。

当年轻代中的 Eden 区分配满的时候,就会触发年轻代的 GC(Minor GC)。具体过程如下:

  • 在 Eden 区执行了第一次 GC 之后,存活的对象会被移动到其中一个 Survivor 分区(以下简称from);
  • Eden 区再次 GC,这时会采用复制算法,将 Eden 和 from 区一起清理。存活的对象会被复制到 to 区;接下来,只需要清空 from 区就可以了。

所以在这个过程中,总会有一个 Survivor 分区是空置的。Eden、from、to 的默认比例是 8:1:1,所以只会造成 10% 的空间浪费。

这个比例,是由参数 -XX:SurvivorRatio 进行配置的(默认为 8)。

老年代

一般用的是标记清除、标记整理算法 对应的是Major GC

2.STW

STW就是stop the world,在垃圾回收的时候整个世界都停止了。

但是他是解决什么问题的呢?(垃圾回收的时候有新的类进来)

3.各个GC

  • Minor GC:发生在年轻代的 GC。
  • Major GC:发生在老年代的 GC。
  • Full GC:全堆垃圾回收。比如 Metaspace 区引起年轻代和老年代的回收。

CMS垃圾回收器

Mostly Concurrent Mark and Sweep Garbage Collector(主要并发­标记­清除­垃圾收集器)

年轻代使用复制算法,而对老年代使用标记-清除算法

CMS 使用的是 Sweep 而不是 Compact,所以它的主要问题是碎片化。随着 JVM 的长时间运行,碎片化会越来越严重,只有通过 Full GC 才能完成整理。

清理过程

1.首先第一步 会进行初始标记,是STW的,但是只标记GCRoot,所以很快

2.第二步并发标记

这个过程会持续比较长的时间,但却可以和用户线程并行。在这个阶段的执行过程中,可能会产生很多变化:

  • 有些对象,从新生代晋升到了老年代;
  • 有些对象,直接分配到了老年代;
  • 老年代或者新生代的对象引用发生了变化。

3.并发清除

4.最终标记

5.并发清除

6.并发重置(Concurrent Reset) 为下一次的GC做准备

CMS的缺点

由于 CMS 在执行过程中,用户线程还需要运行,那就需要保证有充足的内存空间供用户使用。如果等到老年代空间快满了,再开启这个回收过程,用户线程可能会产生“Concurrent Mode Failure”的错误,这时会临时启用 Serial Old 收集器来重新进行老年代的垃圾收集,这样停顿时间就很长了(STW)。

CMS 提供了两个参数来解决这个问题:

(1) UseCMSCompactAtFullCollection(默认开启),表示在要进行 Full GC 的时候,进行内存碎片整理。内存整理的过程是无法并发的,所以停顿时间会变长。

(2)CMSFullGCsBeforeCompaction,每隔多少次不压缩的 Full GC 后,执行一次带压缩的 Full GC。默认值为 0,表示每次进入 Full GC 时都进行碎片整理。

G1垃圾回收器

了解了其他的垃圾回收器 我们来看G1回收器

有没有一种办法,能够首先定义一个停顿时间,然后反向推算收集内容呢?就像是领导在年初制定 KPI 一样,分配的任务多就多干些,分配的任务少就少干点。

重要的参数

  1. -XX:MaxGCPauseMillis 我们要求 G1,在任意 1 秒的时间内,停顿不得超过 10ms,这就是在给它制定 KPI。G1 会尽量达成这个目标,它能够推算出本次要收集的大体区域,以增量的方式完成收集。

这也是使用 G1 垃圾回收器不得不设置的一个参数:

-XX:MaxGCPauseMillis=10

  1. -XX:G1HeapRegionSize=M G1是没有实际上的年轻代和老年代的分区的,只有逻辑上的。

image.png 可以看到G1把堆分成一个个小堆区,叫Region。每一块 Region,大小都是一致的,它的数值是在 1M 到 32M 字节之间的一个 2 的幂值数。

(可以看到上面的黄色区域,相当于是存储大的对象的Region,Humongous Region)

3.-XX:NewRatio 在逻辑上,G1 分为年轻代和老年代,但它的年轻代和老年代比例,并不是那么“固定”,为了达到 MaxGCPauseMillis 所规定的效果,G1 会自动调整两者之间的比例。 -XX:NewRatio就是调整两者的比例的,当然如果你同时设置了MaxGCPauseMillis和NewRatio可能两个都不起作用。

G1垃圾回收过程分类

主要分三大类: (1)G1“年轻代”的垃圾回收,同样叫 Minor GC,这个过程和我们前面描述的类似,发生时机就是 Eden 区满的时候。

(2)老年代的垃圾收集,严格上来说其实不算是收集,它是一个“并发标记”的过程,顺便清理了一点点对象。

(3)真正的清理,发生在“混合模式”,它不止清理年轻代,还会将老年代的一部分区域进行清理。 image.png

RSet

RSet 是一个空间换时间的数据结构。 它的全称是 Remembered Set,用于记录和维护 Region 之间的对象引用关系。

对比之前的Card Table 是一种 points-out(我引用了谁的对象)的结构。而 RSet 记录了其他 Region 中的对象引用本 Region 中对象的关系,属于 points-into 结构(谁引用了我的对象),有点倒排索引的味道。

你可以把 RSet 理解成一个 Hash,key 是引用的 Region 地址,value 是引用它的对象的卡页集合

有了这个数据结构,在回收某个 Region 的时候,就不必对整个堆内存的对象进行扫描了。它使得部分收集成为了可能。

具体的回收过程

年轻代回收

年轻代回收是一个 STW 的过程,它的跨代引用使用 RSet 数据结构来追溯,会一次性回收掉年轻代的所有 Region。

JVM 启动时,G1 会先准备好 Eden 区,程序在运行过程中不断创建对象到 Eden 区,当所有的 Eden 区都满了,G1 会启动一次年轻代垃圾回收过程。

img

并发标记(Concurrent Marking)

当整个堆内存使用达到一定比例(默认是 45%),并发标记阶段就会被启动。这个比例也是可以调整的,通过参数 -XX:InitiatingHeapOccupancyPercent 进行配置。

Concurrent Marking 是为 Mixed GC 提供标记服务的,并不是一次 GC 过程的一个必须环节。这个过程和 CMS 垃圾回收器的回收过程非常类似,你可以类比 CMS 的回收过程看一下。

这个过程不需要 STW。如果发现 Region 里全是垃圾,在这个阶段会立马被清除掉。不全是垃圾的 Region,并不会被立马处理,它会在 Mixed GC 阶段,进行收集。

混合回收(Mixed GC)

能并发清理老年代中的整个整个的小堆区是一种最优情形。混合收集过程,不只清理年轻代,还会将一部分老年代区域也加入到 CSet 中。

通过 Concurrent Marking 阶段,我们已经统计了老年代的垃圾占比。在 Minor GC 之后,如果判断这个占比达到了某个阈值,下次就会触发 Mixed GC。这个阈值,由 -XX:G1HeapWastePercent 参数进行设置(默认是堆大小的 5%)。因为这种情况下, GC 会花费很多的时间但是回收到的内存却很少。所以这个参数也是可以调整 Mixed GC 的频率的。

还有参数 G1MixedGCCountTarget,用于控制一次并发标记之后,最多执行 Mixed GC 的次数。