深入理解G1回收器——概念详解
⭐⭐⭐⭐⭐⭐
Github主页👉https://github.com/A-BigTree
笔记链接👉https://github.com/A-BigTree/Code_Learning
⭐⭐⭐⭐⭐⭐
如果可以,麻烦各位看官顺手点个star~😊
文章目录
- 深入理解G1回收器——概念详解
- 1 堆内存模型
- 2 G1回收机制
- 3 RSet&CSet
- 4 SATB
- 5 可预测停顿
G1(Garbage-First),它是一款面向服务端应用的垃圾收集器,在多 CPU 和大内存的场景下有很好的性能。HotSpot 开发团队赋予它的使命是未来可以替换掉 CMS 收集器。
1 堆内存模型
堆被分为新生代和老年代,其它收集器进行收集的范围都是整个新生代或者老年代,而 G1 可以直接对新生代和老年代一起回收。
G1 把堆划分成多个大小相等的独立区域(Region),新生代和老年代不再物理隔离。
通过引入 Region 的概念,从而将原来的一整块内存空间划分成多个的小空间,使得每个小空间可以单独进行垃圾回收。这种划分方法带来了很大的灵活性,使得可预测的停顿时间模型成为可能。通过记录每个 Region 垃圾回收时间以及回收所获得的空间(这两个值是通过过去回收的经验获得),并维护一个优先列表,每次根据允许的收集时间,优先回收价值最大的 Region。
2 G1回收机制
G1 收集器的运作大致分为以下几个步骤:
- 初始标记
- 并发标记
- 最终标记
- 筛选回收
在垃圾回收时,G1 的运行方式与 CMS 收集器类似。G1 并发执行全局标记来确定整个堆中对象的活跃度。标记阶段完成后,G1 知道哪些区域大部分是空的。它首先在这些区域进行收集,这通常会产生大量可用空间,这是也为什么G1回收机制被称为垃圾回收优先。G1将收集和压缩的操作集中在可能充满可回收对象(即垃圾)的区域上。同时G1 使用停顿预测模型来满足用户定义的暂停时间,并根据指定的暂停时间选择要收集的区域( Region)数量。
G1 将对象从堆的一个或多个区域复制到堆上的单个区域,并在此过程中压缩和释放内存。这个复制清除操作会并行执行来减少暂停时间并提高吞吐量。所以每次垃圾收集时,G1 都会在用户定义的暂停时间内持续工作来减少碎片空间
与CMS和ParallelOld的区别。
- CMS(并发 标记清除 )垃圾收集器不进行整理或复制。
- ParallelOld 垃圾收集是对整个堆进行整理,会造成相当长的SWT。
3 RSet&CSet
每个 Region 都有一个 Remembered Set(RSet),用来记录该 Region 对象的引用对象所在的 Region。通过使用 Remembered Set,在做可达性分析的时候就可以避免全堆扫描。
还有一种数据结构也是辅助GC的:Collection Set(CSet),它记录了GC要收集的Region集合,集合里的Region可以是任意年代的。
RSet与Card Table有些类似,是一种典型的空间换时间工具。在GC的时候,对于old->young和old->old的跨代对象引用,只要扫描对应的CSet中的RSet即可。 逻辑上说每个Region都有一个RSet,RSet记录了其他Region中的对象引用本Region中对象的关系,属于points-into结构(谁引用了我的对象)。而Card Table则是一种points-out(我引用了谁的对象)的结构,每个Card 覆盖一定范围的Heap(一般为512Bytes)。G1的RSet是在Card Table的基础上实现的:每个Region会在对应的RSet中记录下别的Region有指向自己的指针,并标记这些指针分别在哪些Card的范围内。 这个RSet其实是一个Hash Table,Key是别的Region的起始地址,Value是一个集合,里面的元素是Card Table的Index。
RSet是怎么配合CSet进行快速垃圾分析的?
GC时GC Root的对象只可能在两个位置:
- CSet里的Region
- 非CSet里的Region
CSet是需要收集的region集合而非CSet里的region不需要收集,如果GC Root对象在CSet的Region里,依次遍历可达对象即可;如果GC Root在非CSet里,就需要依次遍历从非CSet到CSet里对象的引用即扫描所有非CSet region,非常耗时。
如果有了RSet,通过扫描CSet里所有Region的RSet就能知道不参与收集的其他Region对CSet中对象的引用,避免了全局扫描这些不参与收集的Region(有点绕😵💫)
RSet和CSet的引入不会影响JVM堆的利用率吗?
根据官网介绍,使用G1的JVM进程会更大一些,但RSets 的整体占用空间影响小于 5%,同时CSet中的所有活动数据在 GC 期间都会被清除(复制/移动),对 JVM 空间的影响不到 1%。
4 SATB
全称是Snapshot-At-The-Beginning,由字面理解,是GC开始时活着的对象的一个快照。它是通过Root Tracing得到的,作用是维持并发GC的正确性。 那么它是怎么维持并发GC的正确性的呢?根据三色标记算法,我们知道对象存在三种状态:
- 白:对象没有被标记到,标记阶段结束后,会被当做垃圾回收掉;
- 灰:对象被标记了,但是它的field还没有被标记或标记完;
- 黑:对象被标记了,且它的所有field也被标记完了;
由于并发阶段的存在,Mutator线程(应用/用户线程)和Garbage Collector线程同时对对象进行修改,就会出现白对象漏标的情况,这种情况发生的前提是:
- 有至少一个黑色对象在自己被标记之后指向了这个白色对象
- 所有的灰色对象在自己引用扫描完成之前删除了对白色对象的引用
对于第一个条件,在并发标记阶段,如果该白对象是new出来的,并没有被灰对象持有,那么它会不会被漏标呢?Region中有两个top-at-mark-start(TAMS)指针,分别为prevTAMS和nextTAMS。在TAMS以上的对象是新分配的,这是一种隐式的标记。对于在GC时已经存在的白对象,如果它是活着的,它必然会被另一个对象引用,即条件二中的灰对象。如果灰对象到白对象的直接引用或者间接引用被替换了,或者删除了,白对象就会被漏标,从而导致被回收掉,这是非常严重的错误,所以SATB破坏了第二个条件。也就是说,一个对象的引用被替换时,可以通过write barrier 将旧引用记录下来。
这种方式有个缺点,就是会产生浮动垃圾。 因为当用户线程取消引用的时候,有可能是真的取消引用,对应的对象是真的要回收掉的。这时候我们通过这种方式,就会把本该回收的对象又复活了,从而导致出现浮动垃圾。但相对于本该存活的对象被回收,这个代价还是可以接受的,毕竟在下次 GC 的时候就可以回收了。
CMS也有并发标记过程,它是怎么解决这个问题的呢?
CMS 回收器采用的是增量更新方案,即破坏第一个条件:「有至少一个黑色对象在自己被标记之后指向了这个白色对象」。
既然有黑色对象在自己标记后,又重新指向了白色对象。那么我就把这个黑色对象的引用记录下来,在后续「重新标记」阶段再以这个黑色对象为根,对其引用进行重新扫描。通过这种方式,被黑色对象引用的白色对象就会变成灰色,从而变为存活状态。
这种方式有个缺点,就是会重新扫描新增的这部分黑色对象,会浪费多一些时间。但是这段时间相对于并发标记整个链路的扫描,还是小巫见大巫,毕竟真正发生引用变化的黑色对象是比较少的。
5 可预测停顿
Pause Prediction Model 即停顿预测模型。它在G1中的作用是:
G1 uses a pause prediction model to meet a user-defined pause time target and selects the number of regions to collect based on the specified pause time target.
G1 GC是一个响应时间优先的GC算法,它与CMS最大的不同是,用户可以设定整个GC过程的期望停顿时间,参数-XX:MaxGCPauseMillis
指定一个G1收集过程目标停顿时间,默认值200ms
,不过它不是硬性条件,只是期望值。那么G1怎么满足用户的期望呢?就需要这个停顿预测模型了。G1根据这个模型统计计算出来的历史数据来预测本次收集需要选择的Region数量,从而尽量满足用户设定的目标停顿时间。 停顿预测模型是以衰减标准偏差为理论基础实现的。
(有点复杂,感兴趣的小伙伴可以自己深入探索一下😵💫)