Panda 白话 - G1垃圾收集器 之 RSet(Remembed Set)源码解读

G1的知识点越看越多,这个RSet和卡表老也整不明白,单拎出来白话一下吧~

我们已经知道G1将堆内存划分为2048个(默认、可调整)大小相等的Region,新创建的对象都是放在新生代Eden区。

Region分为5中类型:

  • YHR - Yound Heap Reagion :年轻代分区
    • ERH - Eden Heap Region : eden区,伊甸园,放新创建对象
    • SRH - Survivor Heap Region : Survivor 区,存货去,放每次GC后存活对象
  • OHR - Old Heap Region : 老年代分区,放长命对象
  • HHR - Humongous Heap Region : 巨型对象分区,存放巨大(>Region Size 的 50%)对象
  • FHR - Free Heap Region :空闲分区,还未进行分配

RSet - Remembed Set :记忆集

记忆集嘛,记得是什么呢?记录Region 间的引用关系,

Region间引用关系分为5种:

  • Region内部引用 : 存放再一个Region内的对象间引用 - 不需要记录
  • Young Region -> Young Region:年轻代引用年轻代 - 不需要记录
  • Young Region -> Old Region: 年轻代引用老年代 - 不需要记录
  • Old Region -> Young Region:老年代引用年轻代 - 需要记录
  • Old Region -> Old Region: 老年代引用老年代 - 需要记录

可以看到G1垃圾收集器做了三重过滤
我们来看一下为啥前三个不需要记录:

RH  ->  RH - GC是以Region为最小单位,回收时会扫描整个Region,
			 也就是Region内每个对象都会扫描到,谁引用谁自然知道了,无需额外空间记录
			 在处理RSet时过滤
			 
YRH -> YRH - G1提供了三种GC-YGC、Mixed GC、Full GC,每种GC都会全量回收新生代Region,
		     新生代中每个Region都会被整个扫描,代间引用就不用记了
		     在写屏障时过滤
		     
YRH -> ORH - YGC:只回收新生代,与老年代无关,不用记;
			 Mixed GC : 以新生代为根,根可达分析的时候就找到老生代了,不用记
			 Full GC : 整堆回收,还记啥了
			 在写屏障时过滤

引用的概念:

刚开始刷G1的时候看到RSet是一脸懵的,一时间反应不过来代间引用是啥东西,给同样发懵的你点一手,嘎嘎~

public class Panda {
    private Kongfu kongfu;

    public static void main(String[] args) {
        Panda panda = new Panda();
        Kongfu kongfu = new Kongfu();
        panda.kongfu = kongfu;
    }
}
class Kongfu {}

上面代码不要太简单了,那我们来解析一下,<{=....(嘎嘎~)

  • 有一个类Panda,Panda类有个属性是Kongfu类型的
  • 有个类Kongfu
  • 有个main方法,创建了Panda对象、Kongfu对象,将Kongfu对象的引用赋值给Panda对象的kongfu属性
    此时Panda对象就有一个到Kongfu对象的引用了,以上解析简直是听君一席话如听一席话,那我们再来图解一下,嘎嘎~
    在这里插入图片描述

上面是以堆得维度的图示,我们再来看下Region维度的引用关系,

在这里插入图片描述
上图示意:

  • 两个年轻代Region:YHR1-有两个对象Obj1、Obj2; YHR2 -有一个对象Obj1
  • 两个老年代Region:OHR1 - 有一个对象Obj2、OHR2 - 有一个对象Obj1
  • Obj1_YHR1.field1 = obj1_YHR2 - 年轻代YHR1 中 Obj1 对象 中 field1属性引用 年轻代YHR2中Obj1对象
  • Obj1_YHR1.field2 = obj2_OHR1 - 年轻代YHR1 中 Obj1 对象 中 field2属性引用 老年代代OHR1中Obj2对象
  • Obj1_OHR2.field1 = obj2_OHR1 - 老年代OHR1 中 Obj1 对象 中 field1属性引用 老年代OHR1中Obj2对象
  • Obj2_OHR1.field1 = obj2_YHR1 - 老年代OHR1 中 Obj2 对象 中 field1属性引用 年轻代YHR1中Obj2对象
  • Obj2_OHR1.field2 = obj1_YHR2 - 老年代OHR1 中 Obj2 对象 中 field2属性引用 年轻代YHR2中Obj1对象
  • Obj2_OHR1.field3 = obj1_OHR2 - 老年代OHR1 中 Obj2 对象 中 field3属性引用 老年代OHR2中Obj1对象
    还是看图说话吧:
    在这里插入图片描述
    现在已经知道啦引用的概念,代间引用的概念~
    下面我们来康康RSet是怎么记录这种引用关系的呢~

RSet 采用的是Point In的方式记录引用,Point In就是向里指嘛,就是谁指向了我,如上面的代码小例,Panda对象引用Kongfu对象,那么Kongfu对象所在的Region的RSet就会这个引用关系
Point In 方式有个问题,就是谁都可以引用我,如果我火了,指向我的对象越来越多,每个引用记录都得记啊,那我的RSet不爆表了吗,这个时候记录引用的额外空间的开销就太大了,

G1将RSet数据结构设置成动态变化的,有三种结构:

  1. 稀疏哈希表 - SparsePRT
  2. 细粒度 - PerRegionTable
  3. 粗粒度位图 - BitMap

RSet 数据结构-源码:HeapRegionRemSet.hpp

class OtherRegionsTable VALUE_OBJ_CLASS_SPEC {
	BitMap      _coarse_map;//粗粒度位图
	PerRegionTable** _fine_grain_regions;//细粒度PRT
	SparsePRT   _sparse_table;//稀疏哈希表
}

先补充个知识,打个提前量~
让我们先来康康卡表是啥子~~

Card Table - 卡表:

卡表就是将Region进一步划分,将Region划分为若干个物理连续的512Byte的card page - 卡页,这样每个Region就有一个卡表来映射Regin中的卡页,整堆有个global card table - 全局卡表 存储所有Region的卡表情况
这个老哥的图画的很明白,盗图如下:Heap -> Regions组成 -> Card Pages 组成

老哥的例子是:

  • Heap - 堆 大小设置为 1GB
  • 那么global card table - 全局卡表 的长度就是 1GB / 512Byte = 2097151 个
  • HR - Heap Region - 每个Region大小设置为 1MB
  • 那么每个Region 的卡表长度就是 1MB / 512Byte = 2048 个
  • 没毛病~
    在这里插入图片描述

回到正文,康康RSet都存储了啥子~

哈希表:默认长度是4

 key  - region address  引用对象所在Region的地址
 value - card page index array - 引用对象所在Region中卡表索引值数组,

key - 存储到Region级别的,我得知道哪个Region引用了我啊
value - card 级别的,我得知道引用我得对象在Region中具体那个位置啊
没毛病啊~

结构定义-源码:SparsePRTEntry

  RegionIdx_t _region_ind;
  int         _next_index;
  card_elem_t _cards[card_array_alignment];

像哈希表中添加引用 - 源码:SparsePRT

bool RSHashTable::add_card(RegionIdx_t region_ind, CardIdx_t card_index) {
  //根据regionId获取SparsePRTEntry
  SparsePRTEntry* e = entry_for_region_ind_create(region_ind);
  assert(e != NULL && e->r_ind() == region_ind,"Postcondition of call above.");
  //添加卡表索引到SparsePRTEntry
  SparsePRTEntry::AddCardResult res = e->add_card(card_index);
  if (res == SparsePRTEntry::added) _occupied_cards++;
#if SPARSE_PRT_VERBOSE
  gclog_or_tty->print_cr("       after add_card[%d]: valid-cards = %d.",
                         pointer_delta(e, _entries, SparsePRTEntry::size()),
                         e->num_valid_cards());
#endif
  assert(e->num_valid_cards() > 0, "Postcondition");
  return res != SparsePRTEntry::overflow;
}

这个老哥的图画的也不错~
在这里插入图片描述panda举个更接地气的示例吧,看图说话:

在这里插入图片描述

  • B、C、D 分区中的对象b、c、d都引用了A分区中的a对象
  • Point In方式记录,所以Region A 中的RSet会记录这个3个引用情况
  • key分别记录 B、C、D 分区地址
  • value 分别记录b、c、d对象所在卡页对应卡表的index
  • c对象比较大,占3个card page,所以value是数组类型,存放index的值

引出下面的结构前先来思考一个问题:
稀疏哈希表的结构好像挺好,引用对象在哪个分区,分区中哪个card上,都记录上了,为什么还要设计细粒度、粗粒度PRT结构呢??

我们以上图a对象所在Region A 的RSet为例:
当前a对象有3个引用,RSet记录情况为:

  • region B-> [1]
  • region D-> [122]
  • region C -> [2544,2545,2546]

一个整型占4Byte,当前RSet开销已经>32Byte,当引用逐渐增多,RSet占用的内存空间就太大了,要知道RSet并不是对象本身的数据,开销太大得不偿失了。。

在这里插入图片描述

细粒度PerRegionTable:

基于上面情况的考虑,G1将稀疏哈希表的默认长度设置为4,
当引用Region数超过4个时,就会进行粒度降级(panda自造名称,意会即可,嘎嘎~),什么意思呢?
就是哈希表不是存索引值来找对应的card吗,我们现在用更小的单位来映射card,
位图来映射卡表,
位图很好理解,位-Bit,最小单位,只有0和1,位图就是用0和1来代表一些信息,

位图来映射卡表就是:

  • 一个Bit代表一个card(512Byte) , 8Bit = Byte ,你看看,1:4096的比例
  • 0代表这个卡表中对象没有引用
  • 1代表有引用
  • 中心思想就是用更小的空间来代表card中对象应用情况

还是看图说话吧,一目了然:
在这里插入图片描述

好,道理都懂了,康康PRT具体咋存的吧:

结构定义-源码:PerRegionTable

HeapRegion*     _hr;//Heap Region指针
  CHeapBitMap     _bm; //CHeap 位图,每一位映射Region中一个card
  jint            _occupied;//引用数量
  PerRegionTable* _next;
  PerRegionTable* _prev;
  PerRegionTable * _collision_list_next;
  static PerRegionTable* volatile _free_list;

向PRT中添加引用 - 关键源码:

void add_reference_work(OopOrNarrowOopStar from, bool par) {
	//获取HeapRegion
    HeapRegion* loc_hr = hr();
    if (loc_hr->is_in_reserved_raw(from)) {
      size_t hw_offset = pointer_delta((HeapWord*)from, loc_hr->bottom());
      //获取卡表索引
      CardIdx_t from_card = (CardIdx_t)hw_offset >> (CardTableModRefBS::card_shift - LogHeapWordSize);
      assert(0 <= from_card && (size_t)from_card < HeapRegion::CardsPerRegion,"Must be in range.");
      //添加卡表工作
      add_card_work(from_card, par);
    }
  }

add_card_work:源码分析

  void add_card_work(CardIdx_t from_card, bool par) {
    if (!_bm.at(from_card)) {
      //此时传的是true
      if (par) {
        // 将卡表索引值对应位图中值设置为1、代表有引用
        if (_bm.par_at_put(from_card, 1)) {
          Atomic::inc(&_occupied);
        }
      } else {
        _bm.at_put(from_card, 1);
        _occupied++;
      }
    }
  }

这位老哥的图仅做参考吧,中心思想是没毛病的:

  • 一个Region一个PRT
  • PRT里面存放位图
  • 被引用Region的RSet存放引用对象所在Region地址和PRT位图
  • 多个引用放到数组里

在这里插入图片描述
大佬给了个概括图,也不错:
在这里插入图片描述

我们来举例说明吧,还是以上面a 、b 、c、d 对象的引用为例:

Region A 的RSet 存储情况就是:

  • region B-> bitmap (01000…)
  • region D-> bitmap (000…1000…)
  • region C -> bitmap (000…111000…)
  • 用位图的1代表这个Region的这个card里有对象引用了我的Region中对象

粗粒度PerRegionTable:

上面细粒度PerRegionTable看上去还不赖,
但是对于火爆对象来说还不行,
太多Region中都有对象不断引用我,那我的PRT记录数一直飙升也扛不住啊,于是乎,G1设置了阈值,达到阈值时继续粒度降级

  • 这个时候位图的0和1不再代表card了
  • 粒度放粗,位图代表Region了
  • 1代表这个Region中有对象引用了我
  • 所以叫粗粒度PerRegionTable

结构定义-源码:OtherRegionsTable

class OtherRegionsTable {
  CHeapBitMap _coarse_map;
}

添加粗粒度位图添加引用 - 源码:OtherRegionsTable

  // Used in the sequential case.
  void add_reference(OopOrNarrowOopStar from) {
    _other_regions.add_reference(from, 0);
  }

对于上图a对象来说:

  • RSet记录一张位图信息:
  • 示例:(000100100001)
  • 位图中三个1分表代表RegionB,RegionC,RegionD引用了RegionA

总结:看图说话
在这里插入图片描述

口说无凭,康康源码吧:

添加引用-关键-源码

panda.kongfu = kongfu
panda对象引用kongfu对象
要在panda对象所在Region的Rset添加一条引用记录

//添加引用方法
void OtherRegionsTable::add_reference(OopOrNarrowOopStar from, uint tid) {

  // 如果是粗粒度,直接返回
  if (_coarse_map.at(from_hrm_ind)) {
    assert(contains_reference(from), "We just found " PTR_FORMAT " in the Coarse table", p2i(from));
    return;
  }

  // 不是粗粒度的话,先看看是不是细粒度PRT
  size_t ind = from_hrm_ind & _mod_max_fine_entries_mask;
  //获取细粒度PRT
  PerRegionTable* prt = find_region_table(ind, from_hr);
  //没有细粒度PRT
  if (prt == NULL) {
  	//上锁,防止并发情况多线程同时访问RSet,
    MutexLockerEx x(_m, Mutex::_no_safepoint_check_flag);
    // 再次确认有没有细粒度PRT、针对并发情况,万一有其它线程这个时候将RSet数据结构转换了呢
    prt = find_region_table(ind, from_hr);
    //还为空,就说明当前还满足稀疏哈希表结构
    if (prt == NULL) {
	  //拿对象所在card的索引值
      CardIdx_t card_index = card_within_region(from, from_hr);
		//像稀疏哈希表添加引用记录(key:region address ,value : card index),添加成功,返回
      if (_sparse_table.add_card(from_hrm_ind, card_index)) {
        assert(contains_reference_locked(from), "We just added " PTR_FORMAT " to the Sparse table", p2i(from));
        return;
      }
	//上一步没返回,说明有事情发生
	//上一步稀疏哈希表失败了,那就看细粒度PRT还能用不
	//判断细粒度PRT是否达到了阈值
      if (_n_fine_entries == _max_fine_entries) {
      	//达到阈值了,不能用了,整个表干掉
        prt = delete_region_table();
        //重新初始化
        prt->init(from_hr, false /* clear_links_to_all_list */);
      } else {
      	//没有细粒度PRT呢,那就申请一个细粒度PRT
        prt = PerRegionTable::alloc(from_hr);
        link_to_all(prt);
      }

	  //程序走到这说明哈希表已经满了,我们已经申请了一个细粒度PRT
	  //那我们要将哈希表引用信息添迁移到细粒度PRT,RSet结构变了,数据不能丢啊
      // 1、获取稀疏哈希表
      SparsePRTEntry *sprt_entry = _sparse_table.get_entry(from_hrm_ind);
      assert(sprt_entry != NULL, "There should have been an entry");
      //2、遍历稀疏哈希表,将引用信息添加到细粒度PRT中
      for (int i = 0; i < sprt_entry->num_valid_cards(); i++) {
        CardIdx_t c = sprt_entry->card(i);
        prt->add_card(c);
      }
      // 哈希表没用了,可以干掉了
      bool res = _sparse_table.delete_entry(from_hrm_ind);
      assert(res, "It should have been there.");
    }
    assert(prt != NULL && prt->hr() == from_hr, "consequence");
  }
 //以上,数据结构转换完毕,将本次新增引用记录添加到细粒度PRT
  prt->add_reference(from);
  //断言说:我们刚刚把引用添加到PRT中啦。。。<{=....(嘎~嘎~嘎~)
  assert(contains_reference(from), "We just added " PTR_FORMAT " to the PRT (%d)", p2i(from), prt->contains_reference(from));
}

源码步骤总结:

  • 当我们添加一个引用panda.kongfu = new Kongfu()时,G1会调这个方法add_reference来添加引用关系到RSet

  • add_reference方法体:

    • 1、 _coarse_map.at(from_hrm_ind) 粗粒度PRT命中,则return

    • prt = find_region_table 获取细粒度prt (包含加锁,再次获取prt)

    • 2、 (prt == NULL) 细粒度PRT为空,则按稀疏哈希表处理

      • card_index = from_card - from_hr_bot_card_index; 获取卡表索引(引用对象所在Region中卡表的索引)
      • add_card(from_hrm_ind, card_index) 添加引用记录到哈希表
        • 添加成功 return
        • 添加失败 打印 sparse table entry overflow 哈希表已经满了,溢出啦。。
    • 3、上面还没return,说明粗粒度PRT、哈希表结构都指不上了,还得细粒度PRT上,_n_fine_entries == _max_fine_entries先判断细粒度PRT是否达到阈值了

      • delete_region_table() 干掉细粒度PRT,init重新初始化
      • alloc(from_hr) 申请一个细粒度PRT
    • 4、数据迁移,将哈希表中引用信息 -> 细粒度PRT

      • *sprt_entry = _sparse_table.get_entry(from_hrm_ind) 获取哈希表
      • 循环哈希表,将引用数据添加到PRTprt->add_card(c);
      • delete_entry(from_hrm_ind) 删除哈希表
    • 5、prt->add_reference(from);添加本次引用到PRT

粗粒度PRT -> 细粒度PRT -> 哈希表 -> 细粒度PRT

end by 2021-11-21 2:05
谁不睡呀,我不睡,我是秃头小宝贝~

  • 9
    点赞
  • 14
    收藏
    觉得还不错? 一键收藏
  • 6
    评论
为什么要学JVM1、一切JAVA代码都运行在JVM之上,只有深入理解虚拟机才能写出更强大的代码,解决更深层次的问题。2、JVM是迈向高级工程师、架构师的必备技能,也是高薪、高职位的不二选择。3、同时,JVM又是各大软件公司笔试、面试的重之重,据统计,头部的30家互利网公司,均将JVM作为笔试面试的内容之一。4、JVM内容庞大、并且复杂难学,通过视频学习是最快速的学习手段。课程介绍本课程包含11个大章节,总计102课时,无论是笔试、面试,还是日常工作,可以让您游刃有余。第1章 基础入门,从JVM是什么开始讲起,理解JDK、JRE、JVM的关系,java的编译流程和执行流程,让您轻松入门。第2章 字节码文件,深入剖析字节码文件的全部组成结构,以及javap和jbe可视化反解析工具的使用。第3章 类的加载、解释、编译,本章节带你深入理解类加载器的分类、范围、双亲委托策略,自己手写类加载器,理解字节码解释器、即时编译器、混合模式、热点代码检测、分层编译等核心知识。第4章 内存模型,本章节涵盖JVM内存模型的全部内容,程序计数器、虚拟机栈、本地方法栈、方法区、永久代、元空间等全部内容。第5章 对象模型,本章节带你深入理解对象的创建过程、内存分配的方法、让你不再稀里糊涂。第6章 GC基础,本章节是垃圾回收的入门章节,带你了解GC回收的标准是什么,什么是可达性分析、安全点、安全区,四种引用类型的使用和区别等等。第7章 GC算法与收集器,本章节是垃圾回收的重点,掌握各种垃圾回收算法,分代收集策略,7种垃圾回收器的原理和使用,垃圾回收器的组合及分代收集等。第8章 GC日志详解,各种垃圾回收器的日志都是不同的,怎么样读懂各种垃圾回收日志就是本章节的内容。第9章 性能监控与故障排除,本章节实战学习jcmd、jmx、jconsul、jvisualvm、JMC、jps、jstatd、jmap、jstack、jinfo、jprofile、jhat总计12种性能监控和故障排查工具的使用。第10章 阿里巴巴Arthas在线诊断工具,这是一个特别小惊喜,教您怎样使用当前最火热的arthas调优工具,在线诊断各种JVM问题。第11章 故障排除,本章会使用实际案例讲解单点故障、高并发和垃圾回收导致的CPU过高的问题,怎样排查和解决它们。课程资料课程附带配套项目源码2个159页高清PDF理论篇课件1份89页高清PDF实战篇课件1份Unsafe源码PDF课件1份class_stats字段说明PDF文件1份jcmd Thread.print解析说明文件1份JProfiler内存工具说明文件1份字节码可视化解析工具1份GC日志可视化工具1份命令行工具cmder 1份学习方法理论篇部分推荐每天学习2课时,可以在公交地铁上用手机进行学习。实战篇部分推荐对照视频,使用配套源码,一边练习一遍学习。课程内容较多,不要一次性学太多,而是要循序渐进,坚持学习。      

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 6
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值