概要
G1垃圾回收器从JDK9开始被认定为默认的垃圾回收器。学习最好的方法就是看官方文档,官方文档如下:
G1官方文档链接
G1源码链接
源码下载指导
核心特色
作为被官方默认的垃圾回收器。G1的主要特点体现在以下几个方面:
1、内存模型采用分区Region
不同于以往的垃圾回收器(serial,parallel,CMS)将堆划分为固定的年轻代,老年代和永久代。

G1将堆划分成一个个大小相等的区域(region),每个区域都可能是 【eden, survivor, old】的任意一种,所以这三种【eden, survivor, old】都不是固定大小,因为每个的数量都可能是任意的。

2、SATB(Snapshot at the Beginning)初始快照
GC开始时,先创建一个对象快照,快照中所有当前活的对象在并发标记时,都认识是活的,并且并发标记过程中所有新建的对戏那个都认为是活的。
其中又有两个关键的位图(prevBitmap和nextBitmap)
源码位置:hotspot/share/gc/g1/g1ConcurrentMark.hpp
G1CMBitMap* _prev_mark_bitmap; // 记录上一轮标记结果的位图 Completed mark bitmap
G1CMBitMap* _next_mark_bitmap; // 记录当前轮次标记的位图 Under-construction mark bitmap
prevBitmap 存储最近一次完整并发标记周期的存活对象信息;
nextBitmap 在并发标记过程中动态更新,最终会覆盖 prevBitmap
位图与Region的关联
_prev_top_at_mark_start(prevTAMS)
_next_top_at_mark_start(nextTAMS)
prevTAMS:指向上一轮标记结束时 Region 的分配位置;
nextTAMS:指向当前轮次标记开始时的分配位置,用于区分隐式存活对

A阶段,初始标记阶段,通过STW,将region的top赋值给nextTAMS;
A-B阶段,并发标记阶段;
B阶段,是并发标记结束阶段。并发标记阶段生成的新对象都会放在[nextTAMS,top]之间,这边对象被定义为“隐式对象”,同时nextBitmap也记录了bottom到nextTAMS之间标记对象的地址;
C阶段,是垃圾清理阶段。会交换prevBitmap和nextBitmap,同时清理[bottom, preTAMS]之间标记后的垃圾,对应的“隐式对象”,在下个阶段才会被标记清理。
初始快照解决的问题:被引用的对象错误的被垃圾回收问题。
问题详细说明:当在并发标记阶段(concurrent mark)使用三色标记算法对对象进行标记时。由于并发标记及CPU调度,所以可能会造成下面情形。
- 时刻1:某个白对象D被灰色对象B引用,那么灰色对象B一定被黑色对象D引用。此时GC线程恰好被终止。
- 时刻2:GC线程重新运行,灰色对象B不再引用白色对象,并且黑色对象D引用了A对象。
此时再扫描就会将A对象作为垃圾被回收。这种错误回收会在D调用A对象的时候报错,为了解决这个问题,G1使用SATB来解决。
SATB机制通过写屏障(write barrier)和并发标记队列解决漏标问题。
源码:
1、写屏障记录旧引用
hotspot/share/gc/g1/g1BarrierSet.cpp
G1BarrierSet::write_ref_array_pre_work(T* dst, size_t count) {
if (!_satb_mark_queue_set.is_active()) return; //检查SATB队列是否启用
T* elem_ptr = dst; //获取数组元素指针
for (size_t i = 0; i < count; i++, elem_ptr++) {
T heap_oop = RawAccess<>::oop_load(elem_ptr); //加在旧引用值
if (!CompressedOops::is_null(heap_oop)) {
enqueue(CompressedOops::decode_not_null(heap_oop)); //将旧引用加入到队列中。
}
}
}
那么他是怎么来实现这一过程的呢?
就是依赖两种结构 Card Table 和 Remember Set(RSet)。
Card Table :在每个region中,每512个字节会被分为一块称之为一个Card,每当这个Card中的发生改变时,那么该Card就会被标记为dirty。
Remember Set:每个region都会维护一个Remember Set记录的就是哪些Card指向了当前region,也就是所谓的dirty Card会被记录在RSet中。
而记录的过程就是通过写屏障(write barrier)来实现的,也就是当引用发生变化时,JVM会注入一段代码,将这个变化保存到RSet中。
但由于RSet是一个region共享的,所以在写RSet的时候,势必会造成竞争写的场景,并且多个线程频繁的切换上下文也同样会导致性能问题。
所以,G1的做法并不是直接去写RSet,而是每个线程去维护一个Dirty Card Queue,只有当满足一定情况,即队列的长度达到一定长度时,才会去讲这些Dirty Card对应的引用写入到RSet中。
为了更有效率的处理Dirty Card Queue,G1把这个队列分成了四个区域,从队首到队尾分别为白、绿、黄、红四个区域。当队列中的数据达到不同的区域会采取不同的措施去处理。开发者也可以通过配置参数来设置每一个区域的大小,参数为:-XX:G1ConcRefinementGreen/Yellow/RedZone=来做配置。
当为white白区时,不会做任何处理。
当为green绿区时,Refinement线程被激活,开始更新RSet。
当为yellow黄区时,全部的Refinement线程被激活。
当为Red红区时,应用线程也会参与到处理队列当中的数据的任务中。
参考列表
1、博客园:修心而结网:G1的SATB
2、腾讯元宝AI工具
3、哔哩哔哩:饥人谷后端开发:新一代垃圾回收器:G1详解



2905

被折叠的 条评论
为什么被折叠?



