主要回答两个问题:

  1. G1的原理,以及G1为什么比传统的GC回收性能好
  2. G1如此完美为什么还有ZGC

一个极端的场景(但是经常发生)
在发生Minor GC时,由于survivor放不下,多出的对象提升到老年代。但是老年代由于碎片问题,发生concurrent mode failure错误。这个时候降级为SerialOld回收器。
一次简单的Minor GC演化成Full GC

G1回收器类似里程碑,不要求每次将垃圾清理完,只需要达到目的即可。
我们要求G1在任意1s内,停顿不得超过1ms,这就是G1不得不设置的参数-XX:MaxGCpauseMills=10

为什么叫G1

G1全称GarbageFirst GC,它在对堆的划分上较之前有区别。之前的回收器都是对某个年代整体收集,时间上自然不好控制。G1将堆切成了很多份,把每一份当做一个小目标。

G1空间划分

如图所示,G1有Eden和Sutvivor的,只不过它在内存在不是连续的。
小份区域大小固定,名字叫做小堆区(Region)。小堆区可以是Eden、也可是Survivor,还可以是Old。所以G1的年轻代和老年代的概念是逻辑上的。

Region大小一致,在1M到32M字节之间的一个2的幂指数。若对象太大,一个Region放不下,就放在Humongous Region中,大小超过50%Region的对象都在这个分配。参数-XX:G1HeapRegionSize=<N>M设置。

回收的时候,垃圾最多的小堆区会优先被收集。

G1的垃圾回收过程

逻辑上,分为老年代和年轻代,比例不固定,为了达到MaxGCPauseMillis效果,会自动调整。
如果强行使用-Xmn或者-XX:NewRation设定比例,那么目标就会失效。
G1回收过程分3类

  1. “年轻代”被回收,同样叫Minor GC,和之前一样,时机是Eden区域满了
  2. 老年代的垃圾收集,实际上是“并发标记”的过程,顺便清理了一点对象
  3. 真正的清理,发生在”混合模式“,不只清理年轻代,还会将老年代的一部分区域清理。

GC日志里,1称为[GC pause(G1 Evacuation Pause)(young)],2称为[GC pasue(G1 Evacuation)(mixed)]。Eva是转移的意思。
三种模式间隔不固定。

RSet

Rset是空间换时间的数据结构。类似卡表功能,让全称是Remembered Set,用于记录和维护Region之间的对象引用关系。

Card Table是points-out(我引用了谁的对象),Rset是points-to(谁引用了我的对象)。

Rest类比为Hash,key是Region地址,value是引用了它的对象的卡页集合

有这种数据结构,回收Region时,不必扫描整堆。
年轻代的Region,它的Rset只保存来自老年代的引用,因为年轻代回收针对所有年轻代的Regon。因此年轻代的Region可能为空。
老年代的Region,它的Rset只保存来自老年代的引用,因为老年代的回收发生在年轻代之后。这是Eden边空了,而在回收过程中会扫描Survivor区,因此没必要保存年轻代的引用。

Rset占用很大空间,约5%或更高,为了维护RSet,程序运行过程中,写入某个字段就会产生一个post-write barrier。为了减少这个开销,将内容放入Rset是异步的。

参数-XX:G1ConcRefinementThreads或者-XX:ParallelGCThreads控制这个异步过程。

具体回收过程

G1还有CSet(Collection Set)--收集集合,保存一次GC中将执行垃圾回收的Region。GC是将CSet中的所有存活数据转移。

年轻代回收

是个STW过程,跨代引用用Region追溯,会一次性回收掉年轻代的所有Region。

JVM启动时,会先准备好Eden区,运行中不断创建对象到Eden中,Eden满了,G1会启动一次年轻代垃圾回收。

年轻代的收集有以下阶段:
(1)扫描根
根,可以看做是GC Roots,加上Rset记录的其他Region的外部引用。
(2)更新RS
处理dirty card queue中的卡页,更新Rset。此阶段完成后,Rset可以准确的反映老年代对所在的内存分段中对象的引用。
(3)处理RS
识别被老年代对象指向的Eden中的对象,这些被指向Eden中的对象被认为存活。
(4)复制对象
收集算法使用的是Copy算法
这个阶段Eden中存活的对像复制到Survivor中空的Region
(5)处理引用
处理Soft、Weak、Phantom、Final、JNI Weak等引用。结束收集。

并发标记

堆内存使用达到一定比例(默认45%),并发标记阶段启动。这个比例可以使用-XX:InitiatingHeapOccupancyPercent配置。
并发标记是为Mixed GC提供服务,不是必须环节,过程如下
(1)初始标记
共用Minor GC暂停,复用root scan操作
(2)Root区扫描
(3)并发标记
从GC Roots开始对heap中对象标记,并行,且收集各个Region的存活对象信息
(4)重新标记
和CMS类似,STW的。标记在并发标记阶段发送变化的对象。
(5)清理阶段
Region全是垃圾则这个阶段立马清理掉,否则在Mixed GC阶段收集。

并发标记阶段,有新的对象变化,怎么办?

由SATB(Snapshot At The Begining)算法保证,保证在并发标阶段的正确性。

快照是逻辑的,有几个指针,将Region分几个区段。并发标记期间分配的对象都会在next TAMS和top之间。

混合回收(Mixed GC)

并发清理老年代中的整个整个的小堆区。并发标记中因为已经统计了老年代垃圾的占比,在Minor GC之后,判断占比达到阈值,下次就会触发Mixed GC。参数-XX:G1HeapWastePercent配置(默认5%),`G1Mixed
GCCountTarget`用于控制一次并发标记后,最多执行Mixed GC的次数。

ZGC

若应用吃内存,部分回收不够,仍要进行整个堆回收,因此G1算法复杂,效率不见得高。最新的ZGC有三个Flag:

  1. 停顿时间不超过10ms
  2. 停顿不是不会随堆增大而增大
  3. 可支持几百M,甚至几T(最大4T)。

ZGC中逻辑上的年轻代和老年代也去掉,只分为一个个page,每次进行GC时,都会对page压缩,所以没有碎片问题。目前ZGC使用较少,只在Linux平台使用。

Last modification:April 1st, 2020 at 09:46 pm