文章目录
G1的概念
G1是一个并行与并发回收器,他把堆内存分割为很多不相关的区域(Region)(物理上不连续),就是在延迟可控的情况下,获得尽可能高的吞吐量,在两者之间尽量保证平衡,其担当起全功能收集器的重任和期望;
-
虽然分成了许多不同的区域(Region),但是仍然延用了分代算法,保留了新生代和老年代的概念;
-
Region和Region之间使用复制算法进行垃圾回收,从整理的宏观角度使用是标记-整理算法进行回收;
-
关注低延时(这一点与CMS类似),达到可预测的停顿,能够支持指定在一个长度为M毫秒的时间片段内,消耗在垃圾收集上的时间大概率不超过N毫秒这样的目标;自己理解为通过增量收集算法实现,将内存分为多个区域,在单位时间内,尽可能的收集一块回收率高的区域;
先看下G1示意图,加深下印象,大概长这样子:
概念补充
1.本地分配缓存(LAB)
每个应用线程和GC线程都会独立的使用分区,进而减少同步时间,提升GC效率,这个分区称为本地分配缓冲区(Lab)
2.线程本地分配内存(TLAB)
应用线程可以独占一个本地缓冲区(TLAB)来创建的对象,而大部分都会落入Eden区域(巨型对象或分配失败除外),因此TLAB的分区属于Eden空间;
3.晋升本地缓冲区PLAB
GC独占的本地缓冲区进行操作,该部分称为晋升本地缓冲区(PLAB)
4.GC线程本地缓冲区(GCLAB)
每次垃圾收集时,每个GC线程同样可以独占一个本地缓冲区(GCLAB)用来转移对象,每次回收会将对象复制到Suvivor空间或老年代空间;
5.point-into
表示谁引用了我,remembered set就是此实现
6.point-out
表示指向别人的关系,card table基于此思想实现
7.Per Region Table
由于RSet的记录要占用分区的空间,如果一个分区非常"受欢迎",那么RSet占用的空间会上升,从而降低分区的可用空间。G1应对这个问题采用了改变RSet的密度的方式,在PRT中将会以三种模式记录引用:
1.稀少:直接记录引用对象的卡片索引
2.细粒度:记录引用对象的分区索引
3.粗粒度:只记录引用情况,每个分区对应一个比特位
8.SATB
SATB (Snapshot At The Beginning,初始快照),是一种将并发标记阶段开始时对象间的引用关系,以逻辑快照的形式进行保存的手段;
简单理解就是,在并发标记时,以当前的引用关系作为基础引用数据,不考虑Mutator并发运行时对引用关系的修改(Snapshot命名的由来),标记时是存活状态就认为是存活状态,同时利用SATB Write Barrier记录引用变化。
要理解G1回收器,记忆集,卡表和写屏障的概念是必须要知道的,加油吧
记忆集和卡表
关于记忆集和卡表的基本概念自己这里只简单概述,详细描述可以看周志明老师的深入理解jvm虚拟机第三版的3.4节
记忆集(Remember Set):为解决对象跨代引用所带来的问题,垃圾收集器在新生代中建立了名为记忆集(Remembered Set)的数据结构,用以避免把整个老年代加进GC Roots扫描范围
卡表(Card Table):记忆集是一种抽象的数据结构,卡表是记忆集的一种实现;
书中有一段这样的描述:
也就是说:
遍历Card Table的话,如果table[index] = 1, 那么就可以通过**“起始地址 + index * 卡页的基本单位(512K)”**来得到脏页对应的需要加入到GCRoots的对象空间;
如上图,老年代中对象y引用了新生代中的对象x,此时Edan区内存满了触发minor gc,此时除了局部变量表/静态属性等要加入GCRoots以外,老年代对象y也应当加入GCRoots;此时我们通过遍历card table数组,发现下标为1对应的page为脏页,那么我们如果通过**“老年代内存起始地址 + 下标 * 卡页的基本单位(512K)”**就可以直接将cardpage2中所有的对象加入到GCRoots,以此避免将整个老年代加入到GCRoots;此基于card table实现的我指向了谁的关系,是point-out的一种实现(见上);
G1中的Rset和Card Table
G1 垃圾收集器将堆内存划分为若干个 Region,每个 Region 分区只能是一种角色,Eden区、Survivor区、Old区老年代的其中一个,同时维护了一个全局的卡表来记录所有的脏页;每个Region的大小在1M到32M之间,Region的数量设置为2的指数倍,默认为2048;
同时按照卡表(card table)的思想逻辑上划分为固定大小(512字节)的连续区域,每个区域称之为卡片 card,因此 card 是堆内存中的最小可用粒度,分配的对象会占用物理上连续的若干个card,当查找对分区内对象的引用时便可通过卡片 card 来查找(见RSet);
*若我们知道堆内存的起始地址,那么 对象的内存地址 = 堆内存起始地址 + (card page index) 512B **
反之 cardpage在card table中的index应为,card page index = (对象的内存地址 - 堆内存起始地址)/ 512B
每个Region都维护了各自的Remember Set来记录其他Region引用了当前Region内对象的对象地址,将各自过程中出现的跨区域引用关系通过key-value的方式记录下来;
如果region 2中对象b引用了region 1中的对象a,那么region1中的RSet中应该记录上的是key = region2的起始地址,value应该为对应的card table的下标999;
注意:G1中并不是所有的引用关系都会记录RSet,实际上只会记录Old区到S区或者Old区到E区的引用;并且不重复添加dirty card
写屏障
既然有了通过RSet来避免扫描整个Old区,那么具体是如何维护的Rset呢?
java中存在写屏障这种概念,写屏障相当于Aop切面的概念,也就是说赋值的前后执行自己预先设定的代码
需要注意的是,按照课程中的讲解,存在赋值的时候,只是保存对象的引用信息到一个Darty Card Queue中,到年轻代垃圾回收的时候才从改队列中取出进行处理来更新卡表;这样就不用在每次复制操作的时候去更新卡表了;
举例来说,每一次将一个老年代对象的引用修改为指向年轻代对象,都会被写屏障捕获,并且记录下来。因此在年轻代回收的时候,就可以避免扫描整个老年代来查找根。
那么加入存在y.obj = x ,y和x处于不同的Region,那么执行赋值代码时流程大致如下:
-
检查引用和被引用新对象是否在同一个区域,是否是老年代对新生代的引用
-
是否重复添加dirty_card
-
将y的所在card page地址添加到x所在region的RSet中