JVM:卡表元素如何维护?(写屏障)

写屏障

上面使用记忆集解决了缩减GC Roots扫描范围的问题,现在又抛出来一个新的问题,卡表元素如何维护的呢?,例如它们何时变脏、谁来把它们变脏等。

何时变脏这个问题应该很明确的,原则上应该发生在引用类型字段赋值的那一刻。

但问题是如何变脏,即如何在对象赋值的那一刻去更新维护卡表呢?

假如是解释执行的字节码,那相对好处理,虚拟机负责每条字节码指令的执行,有充分的介入空间;但在编译执行的场景中呢?经过即时编译后的代码已经是纯粹的机器指令流了,这就必须找到一个在机器码层面的手段,把维护卡表的动作放到每一个赋值操作之中。

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

当时看到这里,还是不明白怎么对即时编译后的代码(已经是纯粹的机器指令流)应用写屏障(机器码指令流都已经生成好了)。

原来应用写屏障后,对即时编译后的代码所生成的指令流,已经包含所有赋值操作相应的更新卡表逻辑的指令了。

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

除了写屏障的开销外,卡表在高并发场景下(基本是必然的)还面临着“伪共享”(False Sharing)问题。伪共享是处理并发底层细节时一种经常需要考虑的问题,现代中央处理器的缓存系统中是以缓存行(Cache Line)为单位存储的,当多线程修改互相独立的变量时,如果这些变量恰好共享同一个缓存行,就会彼此影响(写回、无效化或者同步)而导致性能降低,这就是伪共享问题。

尽管上面解释了一番,u1s1,我当时看到这里还是懵的,因为在此之前我还不知道什么是伪共享。不知道在座的各位有没有跟我一样的…/手动捂脸

如何理解伪共享?

了解过MySQL 的索引结构以及MySQL 内存的应该都知道,查询磁盘时、或者更新内存时也是已页为单位。比如你查询一个主键id 去查库。如果内存中没有,就会去查磁盘。那么在查询磁盘过程中(脑部B+ 树的数据结构),由上往下遍历树索引的的过程中,是一个节点一个节点(一页一页)的加载到内存中的。

为什么以页为单位?我认为是因为空间局部性或者经验法则吧,临近的数据在将来被访问的可能性大。

再回到计算机的缓存,当 CPU 把内存的数据载入 cache 时,会把临近的共 64 Byte 的数据一同放入同一个缓存行,可以理解是以缓存行(Cache Line)为单位存储的。

那这样会存在什么问题?看一下下面这个Java 多线程程序的例子:

public class FalseSharingDemo {
  
    // 前提知识:
  	// 1. Java对象的相邻成员变量大概率也会加载到同一缓存行中
  	// 2. 做一个循环计数, 会把计数变量放到缓存里,就不用每次循环都往内存存取数据了
    private static class Pair {
    	// 所以在一个缓存行中,如果有一个线程在读取a时,会顺带把b带出
    	volatile long a;// 一个缓存行64 字节,a属性 8个字节
    
    	volatile long b;
    }
  
  private static void testPointer(Pair pair) throws InterruptedException {
        long start = System.currentTimeMillis();
        Thread t1 = new Thread(() -> {
            for (int i = 0; i < 100000000; i++) {
                pair.a++;
            }
        }, "对 Pair 对象中的变量 a 进行 ++ 操作");

        Thread t2 = new Thread(() -> {
            for (int i = 0; i < 100000000; i++) {
              	// 由于 b 和 a 在同一个缓存行,会导致线程t2 在读取缓存行会失效。需要重新在内存中重新加载进缓存。
                pair.b++;
            }
        },"对 Pair 对象中的变量 b 进行 ++ 操作");

        t1.start();
        t2.start();
        t1.join();
        t2.join();
        System.out.println("两个线程并发时的总耗时:" + (System.currentTimeMillis() - start);
    }
  
    public static void main(String[] args) throws InterruptedException {
        // 测试肉眼可见的并发伪共享问题
        testPointer(new Pair());
    }
}

运行结果:

两个线程并发时的总耗时:2085

稍微调整下代码:

private static class Pair {
    volatile long a;
    // 新增的属性
    long p1, p2, p3, p4, p5, p6, p7;
    volatile long b;
}

再次运行,运行结果如下:

两个线程并发时的总耗时:279

可以发现,修改前后,程序的运行时间基本相差10 倍。

为什么会这样?回到程序中注释中寻找答案。修改前缓存行如何?修改后缓存行如何变化?口头分析

所以我理解伪共享就是:当多线程修改互相独立的变量时,如果这些变量恰好共享同一个缓存行,就会彼此影响而导致性能降低。(比如上面例子线程t1所在的core1在写回缓存时会把t2 线程core 2的缓存失效,那么在线程t2 得直接写回内存了了。那么下一次t2需要在读取的时候,需要在内存中在读进处理器2的缓存。)(结果就是导致基本每一次读都需要重新读进缓存。)

天下没有免费的午餐,技术在解决一个问题的同时,往往甚至必然会带来另外一个问题。就比如引入缓存,提高了读取性能,但同时带来了并发三个特性之一的可见性问题,又比如为了提高命中率,以缓存行为最小单位,同时也带来了并发时的伪共享问题,反而被拖慢了(引入多线程带来上下文切换开销以及原子性问题。,编译优化带来并发的有序性问题)。所以在采用一项技术的同时,一定要清楚它带来的问题是什么,以及如何规避。

那一般如何解决这个问题:

  1. 如上,我们可以使用数据填充的方式来避免,即单个数据填充满一个CacheLine。这本质是一种空间换时间的做法

  2. Java 8 中已经提供了官方的解决方案,Java 8 中新增了一个注解:@sun.misc.Contended。加上这个注解的类会自动补齐缓存行,需要注意的是此注解默认是无效的,需要在 jvm 启动时设置 -XX:-RestrictContended 才会生效。(ConcurrentHashMap

    存在这个注解的应用)

有了伪共享的基础,我们再回来看卡表在高并发场景下(基本是必然的)如何解决“伪共享”(False Sharing)问题?

假设处理器的缓存行大小为64字节,由于一个卡表元素占1个字节,64个卡表元素将共享同一个缓存行。这64个卡表元素对应的卡页总的内存为32KB(64×512字节),也就是说如果不同线程更新的对象正好处于这32KB的内存区域内,就会导致更新卡表时正好写入同一个缓存行而影响性能。

为了避免伪共享问题,一种简单的解决方案是采用条件的写屏障,先检查卡表标记,只有当该卡表元素未被标记过时才将其标记为变脏。

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

参考文章:

https://blog.csdn.net/AZHELL/article/details/73740048

https://zhuanlan.zhihu.com/p/187593289

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/141267.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

032-从零搭建微服务-定时服务(一)

写在最前 如果这个项目让你有所收获&#xff0c;记得 Star 关注哦&#xff0c;这对我是非常不错的鼓励与支持。 源码地址&#xff08;后端&#xff09;&#xff1a;mingyue: &#x1f389; 基于 Spring Boot、Spring Cloud & Alibaba 的分布式微服务架构基础服务中心 源…

Python 列表 pop()函数使用详解

pop函数使用详解 目录 pop函数使用详解 1、按照索引删除元素 1.1、正数索引 1.2、负数索引 1.3、不指定索引 2、返回被删除的元素 3、不同类型的元素 4、常见错误 pop() 可以「删除」列表中的元素&#xff08;默认最后一个&#xff09;。 语法 list.pop( index ) 参…

Java多线程编程秘籍:各种方案一网打尽,不要错过!

一、多线程实现方式 Java 中实现多线程的方式主要有四种&#xff1a; 继承 Thread 类&#xff1a;这是一种最简单的实现方式&#xff0c;直接继承 Thread 类&#xff0c;重写 run() 方法即可。实现 Runnable 接口&#xff1a;这是一种更加灵活的实现方式&#xff0c;不需要继承…

Zigbee智能家居方案设计

背景 目前智能家居物联网中最流行的三种通信协议&#xff0c;Zigbee、WiFi以及BLE&#xff08;蓝牙&#xff09;。这三种协议各有各的优势和劣势。本方案基于CC2530芯片来设计&#xff0c;CC2530是TI的Zigbee芯片。 网关使用了ESP8266CC2530。 硬件实物 节点板子上带有继电器…

Git精讲(一)

&#x1f4d8;北尘_&#xff1a;个人主页 &#x1f30e;个人专栏:《Linux操作系统》《经典算法试题 》《C》 《数据结构与算法》 ☀️走在路上&#xff0c;不忘来时的初心 文章目录 一、Git初识1、提出问题2、如何解决--版本控制器3、注意事项 二、Git 安装1、Linux-centos2、…

目标检测问题总结

目标检测问题总结 目标检测二阶段和一阶段的核心区别目标检测二阶段比一阶段的算法精度高的原因1. 正负样本不平衡2.样本的不一致性 如何解决目标检测中遮挡问题如何解决动态目标检测FPN的作用如何解决训练数据样本过少的问题IOU代码实现NMS代码实现NMS的改进思路 目标检测二阶…

数据结构-堆排序及其复杂度计算

目录 1.堆排序 1.1 向上调整建堆 1.2 向下调整建堆 2. 两种建堆方式的时间复杂度比较 2.1 向下调整建堆的时间复杂度 2.2 向上调整建堆的时间复杂度 Topk问题 上节内容&#xff0c;我们讲了堆的实现&#xff0c;同时还包含了向上调整法和向下调整法&#xff0c;最后我们…

Linux_磁盘管理_df命令

1、df命令是用来干什么的 df的全称是disk free&#xff0c;意为“磁盘空间”。 使用df命令可以查看系统中磁盘的占用情况&#xff0c;有哪些文件系统&#xff0c;在什么位置&#xff08;挂载点&#xff09;&#xff0c;总空间&#xff0c;已使用空间&#xff0c;剩余空间等。…

C++ [继承]

本文已收录至《C语言和高级数据结构》专栏&#xff01; 作者&#xff1a;ARMCSKGT 继承 前言正文继承的概念及定义继承的概念继承的定义重定义 基类和派生类对象赋值转换派生类中的默认成员函数隐式调用显示调用 继承中的友元与静态成员友元静态成员 菱形继承概念 虚继承原理继…

讲座录播 | 邹磊教授:图数据库的概念和应用

2023年10月16日 由中国计算机学会主办的 “CCF Talk”直播间 进行了题目为 术语解读:“图计算”的内涵与应用 主题直播活动 讲座吸引7708人观看 图作为一种灵活表达复杂关联关系的数据结构&#xff0c;目前已广泛地应用于社会治理、医疗健康、电网分析、计算材料、计算育…

【MySQL】事务(中)

文章目录 事务异常与产出结论手动提交 和自动提交 对 回滚的区别 事务隔离性理论如何理解隔离性&#xff1f;MySQL的隔离级别事务隔离级别的查看设置隔离级别 事务异常与产出结论 在没有启动事务之前&#xff0c;account表中存在孙权和刘备的数据 在启动事务后&#xff0c; 向 …

【LIUNX】配置缓存DNS服务

配置缓存DNS服务 A.安装bind bind-utils1.尝试修改named.conf配置文件2.测试nslookup B.修改named.conf配置文件1.配置文件2.再次测试 缓存DNS服务器&#xff1a;只提供域名解析结果的缓存功能&#xff0c;目的在于提高数据查询速度和效率&#xff0c;但是没有自己控制的区域地…

洛谷P1923 【深基9.例4】求第 k 小的数(java)

import java.io.BufferedReader; import java.io.IOException; import java.io.InputStreamReader; import java.io.StreamTokenizer; import java.util.Arrays; import java.util.Scanner; //输入n个数字ai&#xff0c;输出这些数字的第k小的数。最小的数是第0小。 public cla…

高级数据分析方法与模型

前言 数据思维练习不仅要熟练地掌握了分析工具&#xff0c;还要掌握大量的数据分析方法和模型。 这样得出的结论不仅具备条理性和逻辑性&#xff0c;而且还更具备结构化和体系化&#xff0c;并保证分析结果的有效性和准确性。今天从以下6个维度36种分析模型和方法逐个简略介绍…

工作记录--(用HTTPS,为啥能被查出浏览记录?如何解决?)---每天学习多一点

由于网络通信有很多层&#xff0c;即使加密通信&#xff0c;仍有很多途径暴露你的访问地址&#xff0c;比如&#xff1a; DNS查询&#xff1a;通常DNS查询是不会加密的&#xff0c;所以&#xff0c;能看到你DNS查询的观察者&#xff08;比如运营商&#xff09;是可以推断出访问…

【蓝桥杯选拔赛真题67】Scratch鹦鹉学舌 少儿编程scratch图形化编程 蓝桥杯选拔赛真题解析

目录 scratch鹦鹉学舌 一、题目要求 编程实现 二、案例分析 1、角色分析

(三)七种元启发算法(DBO、LO、SWO、COA、LSO、KOA、GRO)求解无人机路径规划MATLAB

一、七种算法&#xff08;DBO、LO、SWO、COA、LSO、KOA、GRO&#xff09;简介 1、蜣螂优化算法DBO 蜣螂优化算法&#xff08;Dung beetle optimizer&#xff0c;DBO&#xff09;由Jiankai Xue和Bo Shen于2022年提出&#xff0c;该算法主要受蜣螂的滚球、跳舞、觅食、偷窃和繁…

4.CentOS7安装MySQL5.7

CentOS7安装MySQL5.7 2023-11-13 小柴你能看到嘛 哔哩哔哩视频地址 https://www.bilibili.com/video/BV1jz4y1A7LS/?vd_source9ba3044ce322000939a31117d762b441 一.解压 tar -xvf mysql-5.7.26-linux-glibc2.12-x86_64.tar.gz1.在/usr/local解压 tar -xvf mysql-5.7.44-…

高速高精运动控制,富唯智能AI边缘控制器助力自动化行业变革

随着工业大数据时代的到来&#xff0c;传统控制与决策方式无法满足现代数字化工厂对工业大数据分析与决策的需求&#xff0c;AI边缘控制器赋能现代化智慧工厂&#xff0c;实现工业智造与行业变革。 富唯智能AI边缘控制器&#xff0c;基于x86架构的IPC形态产品&#xff0c;通过…

用Powershell实现:删除所有不是与.json文件重名的.jpg文件

# 指定要搜索的目录路径 $directoryPath "C:\path\to\your\directory"# 获取该目录下的所有.jpg和.json文件 $jpgFiles Get-ChildItem -Path $directoryPath -Filter *.jpg $jsonFiles Get-ChildItem -Path $directoryPath -Filter *.json | Select-Object -Expan…