在 JDK1.7 之前,CMS 垃圾收集器是主流的选择。但自从 JDK 6u14 体验版本面世,到 JDK 7u4 版本发行,G1 垃圾收集器逐渐成了主流。
目前的 JDK8 以后的版本,G1 已经相对稳定,且基本上已经取代了 CMS。
G1 GC,全称 Garbage-First Garbage Collector,通过-XX:+UseG1GC 参数来启用。G1 的主要关注点在于达到可控的停顿时间,在这个基础上尽可能提高吞吐量,这一点非常重要。和 CMS 相同的地方在于,它们都属于并发收集器,在大部分的收集阶段都不需要挂起应用程序。区别在于,G1 没有 CMS 的碎片化问题(或者说不那么严重),同时提供了更加可控的停顿时间。
官网关于 G1 的相关描述如下:
翻译过来,大致意思是说:G1 垃圾收集算法主要应用在多 CPU 大内存的服务中,在满足高吞吐量的同时,竟可能的满足垃圾回收时的暂停时间,该设计主要针对如下应用场景:
- 像 CMS 收集器一样,能与应用程序线程并发执行。
- 整理空闲空间更快
- 需要 GC 停顿时间更好预测
- 不希望牺牲大量的吞吐性能
- 不需要更大的 Java Heap
以往的垃圾回收算法,如 CMS,堆内存结构大概如下:
可以看到这些 space 必须是地址连续的空间。而在 G1 的算法中,采用了另外一种完全不同的方式组织堆内存,堆内存被划分为多个大小相等的内存块(Region),每个 Region 是逻辑连续的一段内存,结构如下:
在上图中的一些 Region 标明了 H,它代表 Humongous,这表示这些 Region 存储的是巨大对象(humongous object,H-obj),即大小大于等于 region 一半的对象。H-obj 有如下几个特征:
- H-obj 直接分配到了 old gen,防止了反复拷贝移动。
- H-obj 在 global concurrent marking 阶段的 cleanup 和 full GC 阶段回收。
- 在分配 H-obj 之前先检查是否超过 initiating heap occupancy percent 和 the marking threshold, 如果超过的话,就启动global concurrent marking,为的是提早回收,防止 evacuation failures 和 full GC。
堆内存中的 Region 大小可以通过 -XX:G1HeapRegionSize 参数指定,大小区间只能是 1M、2M、4M、8M、16M 和 32M,总之是 2 的幂次方,如果 G1HeapRegionSize 为默认值,则在堆初始化时计算 Region 的实践大小。
执行垃圾收集时,和 CMS 一样,G1 收集线程在标记阶段和应用程序线程并发执行,标记结束后,G1 也就知道哪些区块基本上是垃圾,存活对象极少,G1 会先从这些区块下手,因为从这些区块能很快释放得到很大的可用空间,这也是为什么 G1 被取名为 Garbage-First 的原因。
G1 使用了停顿预测模型来满足用户指定的停顿时间目标,并基于目标来选择进行垃圾回收的区块数量。G1 采用增量回收的方式,每次回收一些区块,而不是整堆回收。
G1 不是一个实时收集器,它会尽力满足我们的停顿时间要求,但也不是绝对的,它基于之前垃圾收集的数据统计,估计出在用户指定的停顿时间内能收集多少个区块。
G1 有和应用程序一起运行的并发阶段,也有 stop-the-world 的并行阶段。但是,Full GC 的时候还是单线程运行的,所以我们应该尽量避免发生 Full GC。
G1 中有 3 个重要的数据结构。
- SATB。全称是 Snapshot-At-The-Beginning,由字面理解,是 GC 开始时活着的对象的一个快照。它是通过 Root Tracing 得到的,作用是维持并发 GC 的正确性。SATB 的做法精度比较低,可能会造成的 float garbage 比较多。
- Remembered Sets:每个区块都有一个 RSet,用于记录进入该区块的对象引用(如区块 A 中的对象引用了区块 B,区块 B 的 Rset 需要记录这个信息),它用于实现收集过程的并行化以及使得区块能进行独立收集。总体上 Remembered Sets 消耗的内存小于 5%。
- Collection Sets:将要被回收的区块集合。GC 时,在这些区块中的对象会被复制到其他区块中,总体上 Collection Sets 消耗的内存小于 1%。
G1 的运行过程主要包含如下 4 种操作方式:
- YGC(不同于CMS)
- 并发阶段
- 混合模式
- full GC (一般是G1出现问题时发生)
YGC,也就是 young gc。
上图中每个小区块都代表 G1 的一个区域(Region),区块里面的字母代表不同的分代内存空间类型(如[E]Eden,[O]Old,[S]Survivor)空白的区块不属于任何一个分区;G1 可以在需要的时候任意指定这个区域属于 Eden 或是 O 区之类的。
G1 YoungGC 在 Eden 充满时触发,在回收之后所有之前属于 Eden 的区块全变成空白。然后至少有一个区块是属于 S 区的(如图半满的那个区域),同时可能有一些数据移到了 O 区。
Eden 充满,这个我解释一下。G1 中的区域会被逻辑分为 Eden 区,Survivor 区及 Old 区。虽然它们可能不连续,但是每个区块占用的比例是固定的。Eden 充满,也可以说是占用的 Region 超过了一定的比例。
YGC 发生在年轻代的 GC 算法,一般对象(除了巨型对象)都是在 eden region 中分配内存,当所有 eden region 被耗尽无法申请内存时,就会触发一次 young gc,这种触发机制和之前的 young gc 差不多,执行完一 次young gc,活跃对象会被拷贝到 survivor region 或者晋升到 old region 中,空闲的 region 会被放入空闲列表中,等待下次被使用。
上图是一个并发 G1 回收的前后对比。Young 区发生了变化、这意味着在 G1 并发阶段内至少发生了一次 YGC(这点和 CMS 就有区别),Eden 在标记之前已经被完全清空,因为在并发阶段应用线程同时在工作、所以可以看到 Eden 又有新的占用。
一些区域被 X 标记,这些区域属于 O 区,此时仍然有数据存放、不同之处在 G1 已标记出这些区域包含的垃圾最多、也就是回收收益最高的区域。
在并发阶段完成之后实际上 O 区的容量变得更大了(O + X的方块)。这时因为这个过程中发生了 YGC 有新的对象进入所致。此外,这个阶段在 O 区没有回收任何对象:它的作用主要是标记出垃圾最多的区块出来。对象实际上是在后面的阶段真正开始被回收。
G1 并发标记周期可以分成几个阶段、其中有些需要暂停应用线程。第一个阶段是初始标记阶段。这个阶段会暂停所有应用线程,部分原因是这个过程会执行一次 YGC。然后,第二个阶段是 G1 开始扫描根区域,这个过程没有暂停应用线程,是后台线程并行处理的。这个阶段不能被YGC所打断、因此后台线程有足够的 CPU 时间很关键。第三个阶段是 G1 进入了一个并发标记阶段。这个阶段也是完全后台进行的,并发标记阶段是可以被打断的。这个阶段之后会有一个二次标记阶段和清理阶段,这两个阶段同样会暂停应用线程,但时间很短。
并发阶段主要做的是发现哪些区域包含可回收的垃圾最多(标记为X)。
接下来 G1 执行一系列的混合 GC。这个时期因为会同时进行 YGC 和清理上面已标记为 X 的区域,所以称之为混合阶段。
像普通的 YGC 那样、G1 完全清空掉 Eden 同时调整 survivor 区。另外,两个标记也被回收了,他们有个共同的特点是包含最多可回收的对象,因此这两个区域绝对部分空间都被释放了。这两个区域任何存活的对象都被移到了其他区域(和 YGC 存活对象晋升到 O 区类似)。这就是为什么 G1 的堆比 CMS 内存碎片要少很多的原因–移动这些对象的同时也就是在压缩对内存。
最后的 full gc,如果对象内存分配速度过快,mixed(混合) gc 来不及回收,导致老年代被填满,就会触发一次 full gc,G1 的 full gc 算法就是单线程执行的 serial old gc,会导致异常长时间的暂停时间,需要进行不断的调优,尽可能的避免 full gc。
: » 喜欢 G1 垃圾收集器就要放肆,爱上 CMS 垃圾收集器要克制
原创文章,作者:3628473679,如若转载,请注明出处:https://blog.ytso.com/252073.html