提出问题

分代收集理论的时候,会存在为了解决对象跨代引用所带来的的问题。垃圾收集器在新生代中建立啦名字为挤一挤的数据结构,用来避免把整个老年代加进GC roots扫描范围。事 实上并不只是新生代、老年代之间才有跨代引用的问题,所有涉及部分区域收集(Partial GC)行为的 垃圾收集器,典型的如G1ZGCShenandoah收集器,都会面临相同的问题,因此我们有必要进一步理清记忆集的原理和实现方式,以便学习几款最新的收集器相关知识时能更好地理解。

记忆集

记忆集是一种用于记录从非收集区域指向收集区域的指针集合的抽象数据结构。如果我们不考虑 效率和成本的话,最简单的实现可以用非收集区域中所有含跨代引用的对象数组来实现这个数据结构。

Class RememberedSet {Object[] set[OBJECT_INTERGENERATIONAL_REFERENCE_SIZE];
}

以对象指针来实现记忆集的伪代码

这种记录全部含跨代引用对象的实现方案,无论是空间占用还是维护成本都相当高昂。而在垃圾收集的场景中,收集器只需要通过记忆集判断出某一块非收集区域是否存在有指向了收集区域的指针就可以了,并不需要了解这些跨代指针的全部细节。那设计者在实现记忆集的时候,便可以选择更为粗犷的记录粒度来节省记忆集的存储和维护成本,下面列举了一些可供选择(当然也可以选择这个范围以外的)的记录精度:

  • 字长精度:每个记录精确到一个机器字长(就是处理器的寻址位数,如常见的32位或64位,这个 精度决定了机器访问物理内存地址的指针长度),该字包含跨代指针。
  • 对象精度:每个记录精确到一个对象,该对象里有字段含有跨代指针。
  • 卡精度:每个记录精确到一块内存区域,该区域内有对象含有跨代指针。

其中,第三种“卡精度”所指的是用一种称为“卡表”(Card Table)的方式去实现记忆集,这也是目前最常用的一种记忆集实现形式。

卡表

前面定义中提到记忆集其实是一种“抽象”的数据结构,抽象的意思是只定义了记忆集的行为意图,并没有定义其行为的具体实现。卡表就是记忆集的一种具体实现,它定义了记忆集的记录精度、与堆内存的映射关系等。 关于卡表与记忆集的关系,不妨按照Java语言中HashMap与Map的关系来类比理解。

卡表最简单的形式可以只是一个字节数组,而HotSpot虚拟机确实也是这样做的。以下这行代码是HotSpot默认的卡表标记逻辑:

CARD_TABLE [this address >> 9] = 0;

字节数组CARD_TABLE的每一个元素都对应着其标识的内存区域中一块特定大小的内存块,这个内存块被称作“卡页”(Card Page)。一般来说,卡页大小都是以2的N次幂的字节数,通过上面代码可以看出HotSpot中使用的卡页是2的9次幂,即512字节(地址右移9位,相当于用地址除以512)。那如果卡表标识内存区域的起始地址是0x0000的话,数组CARD_TABLE的第0、1、2号元素,分别对应了地址范围为0x0000~0x01FF、0x0200~0x03FF、0x0400~0x05FF的卡页内存块,如下图。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-amGbEfPF-1591171398165)(…/images/image-20200603155024158.png)]

一个卡页的内存中通常包含不止一个对象,只要卡页内有一个(或更多)对象的字段存在着跨代指针,那就将对应卡表的数组元素的值标识为1,称为这个元素变脏(Dirty),没有则标识为0。在垃圾收集发生时,只要筛选出卡表中变脏的元素,就能轻易得出哪些卡页内存块中包含跨代指针,把它们加入GC Roots中一并扫描。

写屏障

解决了如何使用记忆集来缩减GC Roots扫描范围的问题,但还没有解决卡表元素如何维护的问题,例如它们何时变脏、谁来把它们变脏等。

卡表元素何时变脏的答案是很明确的——有其他分代区域中对象引用了本区域对象时,其对应的卡表元素就应该变脏,变脏时间点原则上应该发生在引用类型字段赋值的那一刻。但问题是如何变脏,即如何在对象赋值的那一刻去更新维护卡表呢?假如是解释执行的字节码,那相对好处理,虚拟机负责每条字节码指令的执行,有充分的介入空间;但在编译执行的场景中呢?经过即时编译后的代码已经是纯粹的机器指令流了,这就必须找到一个在机器码层面的手段,把维护卡表的动作放到每一个赋值操作之中。

在HotSpot虚拟机里是通过写屏障(Write Barrier)技术维护卡表状态的。写屏障可以看作在虚拟机层面对“引用类型字段赋值”这个动作的AOP切面,在引用对象赋值时会产生一个环形(Around)通知,供程序执行额外的动作,也就是说赋值的前后都在写屏障的覆盖范畴内。在赋值前的部分的写屏障叫作写前屏障(Pre-Write Barrier),在赋值后的则叫作写后屏障(Post-Write Barrier)。HotSpot虚拟机的许多收集器中都有使用到写屏障,但直至G1收集器出现之前,其他收集器都只用到了写后屏障。下面这段代码是一段更新卡表状态的简化逻辑:

void oop_field_store(oop* field, oop new_value) {// 引用字段赋值操作    *field = new_value;    // 写后屏障,在这里完成卡表状态更新    post_write_barrier(field, new_value);
}

应用写屏障后,虚拟机就会为所有赋值操作生成相应的指令,一旦收集器在写屏障中增加了更新卡表操作,无论更新的是不是老年代对新生代对象的引用,每次只要对引用进行更新,就会产生额外的开销,不过这个开销与Minor GC时扫描整个老年代的代价相比还是低得多的。

除了写屏障的开销外,卡表在高并发场景下还面临着“伪共享”(False Sharing)问题。

伪共享是处理并发底层细节时一种经常需要考虑的问题,现代中央处理器的缓存系统中是以缓存行(Cache Line) 为单位存储的,当多线程修改互相独立的变量时,如果这些变量恰好共享同一个缓存行,就会彼此影响(写回、无效化或者同步)而导致性能降低,这就是伪共享问题。

假设处理器的缓存行大小为64字节,由于一个卡表元素占1个字节,64个卡表元素将共享同一个缓存行。这64个卡表元素对应的卡页总的内存为32KB(64×512字节),也就是说如果不同线程更新的对象正好处于这32KB的内存区域内,就会导致更新卡表时正好写入同一个缓存行而影响性能。为了避免伪共享问题,一种简单的解决方案是不采用无条件的写屏障,而是先检查卡表标记,只有当该卡表元素未被标记过时才将其标记为变脏,即将卡表更新的逻辑变为以下代码所示:

if (CARD_TABLE [this address >> 9] != 0)    CARD_TABLE [this address >> 9] = 0;

在JDK 7之后,HotSpot虚拟机增加了一个新的参数-XX:+UseCondCardMark,用来决定是否开启卡表更新的条件判断。开启会增加一次额外判断的开销,但能够避免伪共享问题,两者各有性能损耗,是否打开要根据应用实际运行情况来进行测试权衡。

记忆集、卡表与写屏障相关推荐

  1. 安全点、安全区域、记忆集、卡表与写屏障

    一.安全点(safe point) 1.1.定义 用户程序执行时并非在代码指令流的任意位置都能够停顿下来开始垃圾收集,而是强制要求必须执行到达特殊位置后才能够暂停,这些位置被称为安全点 ( Safep ...

  2. JVM之记忆集|卡表|写屏障

    背景 分代收集理论的时候,会存在为了解决对象跨代引用所带来的的问题.垃圾收集器在新生代中建立了名为记忆集的数据结构,用来避免把整个老年代加进GC roots扫描范围.事 实上并不只是新生代.老年代之间 ...

  3. HotSpot垃圾算法实现之记忆集与卡表和写屏障

    问题一:对象跨代(区域)引用,GC Roots扫描范围如何界定? 当部分区域进行垃圾收集时,如果非收集区域的对象跨区引用了收集区域的对象(收集区域的对象A可能被非收集区域的对象静态字段B引用,这样对象 ...

  4. jvm根节点枚举、安全点、安全区域、记忆集、卡表、写屏障、并发的可达性分析

    讲具体的实现之前,先说说几个和这些垃圾回收器息息相关的一些知识点,可以有一个更好的理解 1.根节点枚举 也就是可达性分析算法从GC Roots集合中找引用链的过程,可作为GC Roots的节点主要在全 ...

  5. Hotspot细节实现安全区域、记忆集卡表

    Hotspot细节实现 文章目录 Hotspot细节实现 3.安全区域 产生原因 安全区域概念 实现过程 4.记忆集与卡表 产生原因: 记忆集概念 实现过程: 卡精度(卡表) 3.安全区域 产生原因 ...

  6. java 卡表_关于JVM的卡表和写屏障

    我们都知道JVM为了更有效率地清除垃圾,把堆对象分为年轻代和老年代,这就可能存在年轻代和老年代的对象存在互相引用的现象. 年轻代指向老年代的引用我们可以不去关心,因为即使minor gc把年轻代对象清 ...

  7. JVM之记忆集和卡表

    当我们进行young gc时,我们的gc roots除了常见的栈引用.静态变量.常量.锁对象.class对象这些常见的之外,如果老年代有对象引用了我们的新生代对象,那么老年代的对象也应该加入gc ro ...

  8. G1技术细节之记忆集和卡表解决跨代引用问题

    跨代引用面临的问题 首先,产生跨代引用场景是发生YongGC的过程.此时新生代的对象会开始寻找根,看自己是否属于根可达对象,从而判断自己是否是垃圾. 那很多同学就开始有疑惑了?不是判断对象是否存活,应 ...

  9. JVM垃圾回收-记忆集和卡表

最新文章

  1. MPB:农科院牧医所赵圣国组-微生物超高分子量DNA提取方法
  2. mysql常用字段及长度
  3. xcodeproj cannot be opened because the project file cannot be parsed.
  4. 木马入侵查杀 linux
  5. 解决ubuntu下eclipse 经常崩溃的问题
  6. js中null,undefined,false,0,'',[],{}判断方法
  7. 3.6.4python下载安装教程_python 3.6.4安装教程
  8. Vue优化策略_项目上线_02
  9. [置顶]android ListView包含Checkbox滑动时状态改变
  10. 中国象棋程序的设计与实现(七)--心得体会和开发日志
  11. 常见异常与错误 翻译
  12. 洛谷 P2804 神秘数字
  13. linux离线安装pg数据库
  14. 基于arduino的WiFi无线传输
  15. Android Studio实现百度地图定位(显示经纬度和地址)
  16. 解决IIS无响应假死状态
  17. 基于Java的亚马逊“手机”评论爬虫的情感分类分析
  18. 分支定界法需要注意的是什么
  19. QT官网最新的往期版本下载链接
  20. 股票预测论文精读:Astock: a new dataset automated stock trading based on stock-specific news analyzing mod

热门文章

  1. matlab中划出实线框,图纸上限定绘图区域的线框,必须用粗实线画出图框,格式分为留装订边和不留装订边。...
  2. [附源码]计算机毕业设计springboot网上书城网站
  3. 此平台不支持虚拟化的 AMD-V/RVI
  4. 油井动液面远程监测系统,将复杂工作简单化
  5. 【xinfanqie】电脑必须避免的11个注意事项
  6. Windows7中开启nbsp;Administratornbsp;帐…
  7. 如何用jQuery实现五星好评
  8. java 汉字 字母_JAVA获取中文汉字字符串拼音首字母,英文字符不变的工具类
  9. Debian10搭建open虚拟专用网
  10. 解决项目部署到阿里服务器后不能发送短信了