浅谈JVM垃圾收集——记忆集与卡表

2023-10-11 13:50

本文主要是介绍浅谈JVM垃圾收集——记忆集与卡表,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

背景

上一篇文章(浅谈JVM垃圾收集)提到了,当JVM进行垃圾收集时,它是怎么判断对象是否跨代引用的呢?

记忆集与卡表

为解决扫描GC ROOT时遇到对象跨代引用所带来的问题,收集器在新生代上建立一个全局的称为记忆集(Remembered Set)的数据结构

这个结构把老年代划分为若干个小块,标识出老年代哪一块内存会存在跨代引用。当发生 Minor GC 时,只有包含了跨代引用的小块内存中的老年代对象才会加入到 GC Roots 扫描中,避免整个老年代加入到 GC Roots 中。

事实上并不只是新生代、老年代之间才有跨代引用的问题,所有涉及部分区域收集(Partial GC)行为的 垃圾收集器,典型的如G1、ZGC和Shenandoah收集器,都会面临相同的问题。

记忆集是一种用于记录从非收集区域指向收集区域指针集合的抽象数据结构。在垃圾收集的场景中,收集器只需要通过记忆集判断出某一块非收集区域是否存在有指向了收集区域的指针就可以了,并不需要了解这些跨代指针的全部细节。那设计者在实现记忆集的时候,便可以选择更为粗犷的记录粒度来节省记忆集的存储和维护成本,下面列举了一些可供选择(当然也可以选择这个范 围以外的)的记录精度:

  • 字长精度:每个记录精确到一个机器字长(就是处理器的寻址位数,如常见的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的卡页内存块,如下图所示:
在这里插入图片描述
一个卡页的内存中通常包含不止一个对象,只要卡页内有一个(或更多)对象的字段存在着跨代指针,那就将对应卡表的数组元素的值标识为1,称为这个元素变脏(Dirty),没有则标识为0。在垃圾收集发生时,只要筛选出卡表中变脏的元素,就能轻易得出哪些卡页内存块中包含跨代指针,把它们加入GC Roots中一并扫描。那虚拟机是何时让卡表元素变脏呢?它是如何维护卡表元素的呢?

写屏障

卡表元素何时变脏的答案是很明确的——有其他分代区域中对象引用了本区域对象时,其对应的卡表元素就应该变脏,变脏时间点原则上应该发生在引用类型字段赋值的那一刻,把维护卡表的动作放到每一 个赋值操作之中。

在HotSpot虚拟机里是通过写屏障(Write Barrier)技术维护卡表状态的。写屏障可以看作在虚拟机层面对“引用类型字段赋值”这个动作的AOP切 面,在引用对象赋值时会产生一个环形(Around)通知,也就是说赋值的前后都在写屏障的覆盖范畴内。在赋值前的部分的写屏障叫作写前屏障(Pre-Write Barrier),在赋值 后的则叫作写后屏障(Post-Write Barrier)。下面是简化的代码逻辑:

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的内存区域内,就会导致更新卡表时正好写入同一个缓存行而影响性能。

简单的说就是卡表内处于同一缓存行中的元素,若对应的不同卡页的内存中的对象的引用关系发生了变化,部分对象发生了跨代引用,那么对应的卡表数组元素就要从0变为1。但是它们由于处于同一缓存行,导致了CPU并行执行变为串行执行,降低了效率。
伪共享问题是卡表元素更改时处于同一缓存行导致的,诱发的因素是不同卡页内的对象发生了跨代引用。

为了避免伪共享问题,一种简单的解决方案是不采用无条件的写屏障,而是先检查卡表标记,只有当该卡表元素未被标记过时才将其标记为变脏,即将卡表更新的逻辑变为以下代码所示:

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

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

总结

记忆集与卡表

HotSpot虚拟机是用记忆集来记录某块内存区域是否包含跨代引用的对象。记忆集是抽象概念,而卡表是记忆集的实现

卡表是用字节数组实现的,卡表数组的每个元素都是代表某块具体内存区域,这个内存区域叫卡页

卡页的大小是512字节,代表一块特定大小的内存块,若在这块内存块中有一个或多个的跨代指针,则将对应的卡表元素标为1,代表“变脏”,否则为0。当虚拟机扫描卡表元素为1时,便将对应的卡页内存区域加入到GC ROOT中一并扫描。

写屏障

使用写屏障来实现卡表元素变脏。写屏障分为写前屏障和写后屏障,大多数垃圾收集器都是使用写后屏障(G1使用写前屏障)。写后屏障具体表现在对引用对象赋值时,如果是跨代引用,则通过写后屏障将对应的卡表元素变脏

伪共享问题(美团面试)

由于CPU集成的多级缓存中是以缓存行来读取数据的,通过MESI协议保证多个CPU之间的缓存一致性。
伪共享问题是卡表元素更改时处于同一缓存行导致的,诱发的因素是不同卡页内的对象发生了跨代引用,从而使CPU并行执行变为串行执行,降低了并发性能。

举例: 若a、b位于同一缓存行,当CPU1修改a后,若CPU2想修改b,必须先提交CPU1的缓存,然后CPU2再去主存中读取数据。

伪共享问题解决方案:JAVA中的解决方案有填充法Contended 注解

  • 填充法:就是 扩大对象的大小,这样,就可以一个缓冲行中,只存在一个对象!这样,就不会导致结果是串行执行了!
  • Contended 注解法:Java1.8 中提供了Contended注解,使用这个注解,VM必须设置 -XX:-RestrictContended。
    ConcurrentHashMap的内部类CounterCell有用到这个注解
    在这里插入图片描述

记忆集与OopMap的联系与区别
共同点:他们都是用于获取GC ROOT
不同点:OopMap记录的是准确的GC ROOT。而记忆集记录的是 包含跨代引用的GC ROOT 的一块内存,还要再扫描这块内存以得到GC ROOT。

问题讨论
前面说记忆集的时候有说到,老年代中的对象也存在跨代引用。目前只针对老年代进行回收的只有CMS收集器,但是在CMS中,对于老年代存在的跨代引用的对象,CMS并没有在老年代维护一个记忆集,而是把整个新生代加入到GC ROOT扫描?

问题回答
《深入理解Java虚拟机》的第3.5.7节中讲G1垃圾收集器的时候有提到CMS中的卡表,下面是书中的一段话:

相比起来CMS的卡表就相当简单, 只有唯一一份,而且只需要处理老年代到新生代的引用,反过来则不需要,由于新生代的对象具有朝 生夕灭的不稳定性,引用变化频繁,能省下这个区域的维护开销是很划算的。
代价就是当CMS发生Old GC时(所有收集器中只有CMS有针对老年代的Old GC),要把整个新生代作为GC Roots来进行扫描。([6]注释中的话)

可知CMS中只会在新生代建立记忆集,老年代中是没有建立记忆集的。
记忆集:用于记录从非收集区域指向收集区域的指针集合。
所以回收新生代时是有老年代(非收集区域)指向新生代(收集区域)的记忆集,而老年代则需要把整个新生代加入到GC ROOT扫描

这篇关于浅谈JVM垃圾收集——记忆集与卡表的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/188503

相关文章

Spring Security简介、使用与最佳实践

《SpringSecurity简介、使用与最佳实践》SpringSecurity是一个能够为基于Spring的企业应用系统提供声明式的安全访问控制解决方案的安全框架,本文给大家介绍SpringSec... 目录一、如何理解 Spring Security?—— 核心思想二、如何在 Java 项目中使用?——

SpringBoot+RustFS 实现文件切片极速上传的实例代码

《SpringBoot+RustFS实现文件切片极速上传的实例代码》本文介绍利用SpringBoot和RustFS构建高性能文件切片上传系统,实现大文件秒传、断点续传和分片上传等功能,具有一定的参考... 目录一、为什么选择 RustFS + SpringBoot?二、环境准备与部署2.1 安装 RustF

springboot中使用okhttp3的小结

《springboot中使用okhttp3的小结》OkHttp3是一个JavaHTTP客户端,可以处理各种请求类型,比如GET、POST、PUT等,并且支持高效的HTTP连接池、请求和响应缓存、以及异... 在 Spring Boot 项目中使用 OkHttp3 进行 HTTP 请求是一个高效且流行的方式。

java.sql.SQLTransientConnectionException连接超时异常原因及解决方案

《java.sql.SQLTransientConnectionException连接超时异常原因及解决方案》:本文主要介绍java.sql.SQLTransientConnectionExcep... 目录一、引言二、异常信息分析三、可能的原因3.1 连接池配置不合理3.2 数据库负载过高3.3 连接泄漏

javacv依赖太大导致jar包也大的解决办法

《javacv依赖太大导致jar包也大的解决办法》随着项目的复杂度和依赖关系的增加,打包后的JAR包可能会变得很大,:本文主要介绍javacv依赖太大导致jar包也大的解决办法,文中通过代码介绍的... 目录前言1.检查依赖2.更改依赖3.检查副依赖总结 前言最近在写项目时,用到了Javacv里的获取视频

Java实现字节字符转bcd编码

《Java实现字节字符转bcd编码》BCD是一种将十进制数字编码为二进制的表示方式,常用于数字显示和存储,本文将介绍如何在Java中实现字节字符转BCD码的过程,需要的小伙伴可以了解下... 目录前言BCD码是什么Java实现字节转bcd编码方法补充总结前言BCD码(Binary-Coded Decima

SpringBoot全局域名替换的实现

《SpringBoot全局域名替换的实现》本文主要介绍了SpringBoot全局域名替换的实现,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一... 目录 项目结构⚙️ 配置文件application.yml️ 配置类AppProperties.Ja

Java使用Javassist动态生成HelloWorld类

《Java使用Javassist动态生成HelloWorld类》Javassist是一个非常强大的字节码操作和定义库,它允许开发者在运行时创建新的类或者修改现有的类,本文将简单介绍如何使用Javass... 目录1. Javassist简介2. 环境准备3. 动态生成HelloWorld类3.1 创建CtC

JavaScript中的高级调试方法全攻略指南

《JavaScript中的高级调试方法全攻略指南》什么是高级JavaScript调试技巧,它比console.log有何优势,如何使用断点调试定位问题,通过本文,我们将深入解答这些问题,带您从理论到实... 目录观点与案例结合观点1观点2观点3观点4观点5高级调试技巧详解实战案例断点调试:定位变量错误性能分

Java实现将HTML文件与字符串转换为图片

《Java实现将HTML文件与字符串转换为图片》在Java开发中,我们经常会遇到将HTML内容转换为图片的需求,本文小编就来和大家详细讲讲如何使用FreeSpire.DocforJava库来实现这一功... 目录前言核心实现:html 转图片完整代码场景 1:转换本地 HTML 文件为图片场景 2:转换 H