全网最硬核 Java 新内存模型解析与实验 - 5. JVM 底层内存屏障源码分析

本文涉及的产品
云解析 DNS,旗舰版 1个月
公共DNS(含HTTPDNS解析),每月1000万次HTTP解析
全局流量管理 GTM,标准版 1个月
简介: 全网最硬核 Java 新内存模型解析与实验 - 5. JVM 底层内存屏障源码分析
个人创作公约:本人声明创作的所有文章皆为自己原创,如果有参考任何文章的地方,会标注出来,如果有疏漏,欢迎大家批判。如果大家发现网上有抄袭本文章的,欢迎举报,并且积极向这个 github 仓库 提交 issue,谢谢支持~本篇文章参考了大量文章,文档以及论文,但是这块东西真的很繁杂,我的水平有限,可能理解的也不到位,如有异议欢迎留言提出。 本系列会不断更新,结合大家的问题以及这里的错误和疏漏,欢迎大家留言如果你喜欢单篇版,请访问: 全网最硬核 Java 新内存模型解析与实验单篇版(不断更新QA中)如果你喜欢这个拆分的版本,这里是目录:


JMM 相关文档:


内存屏障,CPU 与内存模型相关:


x86 CPU 相关资料:


ARM CPU 相关资料:


各种一致性的理解:


Aleskey 大神的 JMM 讲解:


相信很多 Java 开发,都使用了 Java 的各种并发同步机制,例如 volatile,synchronized 以及 Lock 等等。也有很多人读过 JSR 第十七章 Threads and Locks(地址:https://docs.oracle.com/javase/specs/jls/se17/html/jls-17.html),其中包括同步、Wait/Notify、Sleep & Yield 以及内存模型等等做了很多规范讲解。但是也相信大多数人和我一样,第一次读的时候,感觉就是在看热闹,看完了只是知道他是这么规定的,但是为啥要这么规定,不这么规定会怎么样,并没有很清晰的认识。同时,结合 Hotspot 的实现,以及针对 Hotspot 的源码的解读,我们甚至还会发现,由于 javac 的静态代码编译优化以及 C1、C2 的 JIT 编译优化,导致最后代码的表现与我们的从规范上理解出代码可能的表现是不太一致的。并且,这种不一致,导致我们在学习 Java 内存模型(JMM,Java Memory Model),理解 Java 内存模型设计的时候,如果想通过实际的代码去试,结果是与自己本来可能正确的理解被带偏了,导致误解。

我本人也是不断地尝试理解 Java 内存模型,重读 JLS 以及各路大神的分析。这个系列,会梳理我个人在阅读这些规范以及分析还有通过 jcstress 做的一些实验而得出的一些理解,希望对于大家对 Java 9 之后的 Java 内存模型以及 API 抽象的理解有所帮助。但是,还是强调一点,内存模型的设计,出发点是让大家可以不用关心底层而抽象出来的一些设计,涉及的东西很多,我的水平有限,可能理解的也不到位,我会尽量把每一个论点的论据以及参考都摆出来,请大家不要完全相信这里的所有观点,如果有任何异议欢迎带着具体的实例反驳并留言


8. 底层 JVM 实现分析


8.1. JVM 中的 OrderAccess 定义

JVM 中有各种用到内存屏障的地方:

  1. 实现 Java 的各种语法元素(volatile,final,synchronized,等等)
  2. 实现 JDK 的各种 API(VarHandle,Unsafe,Thread,等等)
  3. GC 需要的内存屏障:因为要考虑 GC 多线程与应用线程(在 GC 算法中叫做 Mutator)的工作方式,究竟是停止世界(Stop-the-world, STW)的方式,还是并发的方式
  1. 对象引用屏障:例如分代 GC,复制算法,年轻代 GC 的时候我们一般是从一个 S 区复制存活对象到另一个 S 区,如果复制的过程,我们不想停止世界(Stop-the-world, STW),而是和应用线程同时进行,那么我们就需要内存屏障,例如;
  2. 维护屏障:例如分区 GC 算法,我们需要维护每个区的跨区引用表以及使用情况表,例如 Card Table。这个如果我们想要应用线程与 GC 线程并发修改访问,而不是停止世界,那么也需要内存屏障


  1. JIT 也需要内存屏障:同样地,应用线程究竟是解释执行代码还是执行 JIT 优化后的代码,这里也是需要内存屏障的。

这些内存屏障,不同的 CPU,不同的操作系统,底层需要不同的代码实现,统一的接口设计是:

f="https://github.com/openjdk/jdk/blob/master/src/hotspot/share/runtime/orderAccess.hpp">源代码地址:orderAccess.hpp



微信图片_20220625211148.jpg


不同的 CPU,不同的操作系统实现是不一样的,结合前面 CPU 乱序表格:


微信图片_20220625211214.jpg


我们来看下 linux + x86 的实现:

f="https://github.com/openjdk/jdk/blob/master/src/hotspot/os_cpu/linux_x86/orderAccess_linux_x86.hpp">源代码地址:orderAccess_linux_x86.hpp


image.png


对于 x86,由于 Load 与 Load,Load 与 Store,Store 与 Store 本来有一致性保证,所以只要没有编译器乱序,那么就天生有 StoreStore,LoadLoad,LoadStore 屏障,所以这里我们看到 StoreStore,LoadLoad,LoadStore 屏障的实现都只是加了编译器屏障。同时,前文中我们分析过,acquire 其实就是相当于在 Load 后面加上 LoadLoad,LoadStore 屏障,对于 x86 还是需要编译器屏障就够了。release 我们前文中也分析过,其实相当于在 Store 前面加上 LoadStore 和 StoreStore,对于 x86 还是需要编译器屏障就够了。于是,我们有如下表格:

我们再看下前面我们经常使用的 Linux aarch64 下的实现:

f="https://github.com/openjdk/jdk/blob/master/src/hotspot/os_cpu/linux_aarch64/orderAccess_linux_aarch64.hpp">源代码地址:orderAccess_linux_aarch64.hpp


image.png


如前面表格里面说,ARM 的 CPU Load 与 Load,Load 与 Store,Store 与 Store,Store 与 Load 都会乱序。JVM 针对 aarch64 没有直接使用 CPU 指令,而是使用了 C++ 封装好的内存屏障实现。C++ 封装好的很像我们前面讲的简易 CPU 模型的内存屏障,即读内存屏障(__atomic_thread_fence(__ATOMIC_ACQUIRE)),写内存屏障(__atomic_thread_fence(__ATOMIC_RELEASE)),读写内存屏障(全内存屏障,__sync_synchronize())。acquire 的作用是作为接收点解包让后面的都看到包里面的内容,类比简易 CPU 模型,其实就是阻塞等待 invalidate queue 完全处理完保证 CPU 缓存没有脏数据。release 的作用是作为发射点将前面的更新打包发出去,类比简易 CPU 模型,其实就是阻塞等待 store buffer 完全刷入 CPU 缓存。所以,acquire,release 分别使用读内存屏障和写内存屏障实现。


LoadLoad 保证第一个 Load 先于第二个,那么其实就是在第一个 Load 后面加入读内存屏障,阻塞等待 invalidate queue 完全处理完;LoadStore 同理,保证第一个 Load 先于第二个 Store,只要 invalidate queue 处理完,那么当前 CPU 中就没有对应的脏数据了,就不需要等待当前的 CPU 的 store buffer 也清空。


StoreStore 保证第一个 Store 先于第二个,那么其实就是在第一个写入后面放读内存屏障,阻塞等待 store buffer 完全刷入 CPU 缓存;对于 StoreLoad,比较特殊,由于第二个 Load 需要看到 Store 的最新值,也就是更新不能只到 store buffer,同时过期不能存在于 invalidate queue 未处理,所以需要读写内存屏障,即全屏障。


8.2. volatile 与 final 的内存屏障源码


我们接下来看一下 volatile 的内存屏障插入的相关代码,以 arm 为例子. 我们其实通过跟踪 iload 这个字节码就可以看出来如果 load 的是 volatile 关键字或者 final 关键字修饰的字段会怎么样,以及 istore就可以看出来如果 store的是 volatile 关键字或者 final 关键字修饰的字段会怎么样

对于字段访问,JVM 中也有快速路径和慢速路径,我们这里只看快速路径的代码:

对应源码:

f="https://github.com/openjdk/jdk/blob/master/src/hotspot/cpu/arm/templateTable_arm.cpp">源代码地址:templateTable_arm.cpp


image.png


image.png

相关文章
|
1月前
|
安全 Java 程序员
深入理解Java内存模型与并发编程####
本文旨在探讨Java内存模型(JMM)的复杂性及其对并发编程的影响,不同于传统的摘要形式,本文将以一个实际案例为引子,逐步揭示JMM的核心概念,包括原子性、可见性、有序性,以及这些特性在多线程环境下的具体表现。通过对比分析不同并发工具类的应用,如synchronized、volatile关键字、Lock接口及其实现等,本文将展示如何在实践中有效利用JMM来设计高效且安全的并发程序。最后,还将简要介绍Java 8及更高版本中引入的新特性,如StampedLock,以及它们如何进一步优化多线程编程模型。 ####
40 0
|
15天前
|
存储 监控 算法
Java JVM 面试题
Java JVM(虚拟机)相关基础面试题
|
1月前
|
存储 监控 算法
深入探索Java虚拟机(JVM)的内存管理机制
本文旨在为读者提供对Java虚拟机(JVM)内存管理机制的深入理解。通过详细解析JVM的内存结构、垃圾回收算法以及性能优化策略,本文不仅揭示了Java程序高效运行背后的原理,还为开发者提供了优化应用程序性能的实用技巧。不同于常规摘要仅概述文章大意,本文摘要将简要介绍JVM内存管理的关键点,为读者提供一个清晰的学习路线图。
|
1月前
|
存储 算法 Java
Java内存管理深度解析####
本文深入探讨了Java虚拟机(JVM)中的内存分配与垃圾回收机制,揭示了其高效管理内存的奥秘。文章首先概述了JVM内存模型,随后详细阐述了堆、栈、方法区等关键区域的作用及管理策略。在垃圾回收部分,重点介绍了标记-清除、复制算法、标记-整理等多种回收算法的工作原理及其适用场景,并通过实际案例分析了不同GC策略对应用性能的影响。对于开发者而言,理解这些原理有助于编写出更加高效、稳定的Java应用程序。 ####
|
1月前
|
存储 监控 算法
Java内存管理的艺术:深入理解垃圾回收机制####
本文将引领读者探索Java虚拟机(JVM)中垃圾回收的奥秘,解析其背后的算法原理,通过实例揭示调优策略,旨在提升Java开发者对内存管理能力的认知,优化应用程序性能。 ####
55 0
|
6月前
|
Java
Java面试题:Java内存模型与并发编程知识点,解释Java中“happens-before”的关系,分析Java中的内存一致性效应(Memory Consistency Effects)及其重要性
Java面试题:Java内存模型与并发编程知识点,解释Java中“happens-before”的关系,分析Java中的内存一致性效应(Memory Consistency Effects)及其重要性
42 0
|
8月前
|
SQL 安全 Java
java单例——Java 内存模型之从 JMM 角度分析 DCL
java单例——Java 内存模型之从 JMM 角度分析 DCL
77 0
|
存储 算法 Java
【Android 内存优化】Java 内存模型 ( Java 虚拟机内存模型 | 线程私有区 | 共享数据区 | 内存回收算法 | 引用计数 | 可达性分析 )
【Android 内存优化】Java 内存模型 ( Java 虚拟机内存模型 | 线程私有区 | 共享数据区 | 内存回收算法 | 引用计数 | 可达性分析 )
265 0
|
存储 缓存 Java
Java高级之内存模型分析
博客出自:http://blog.csdn.net/liuxian13183,转载注明出处! All Rights Reserved ! 下文是博主感悟,请带着怀疑性的态度阅读! 需要了解基本变量所占内存大小,请移步:读书笔记-类结构的认识 Java存储空间有这么几块-来源于Java编程思想 寄存器:位于处理器内部,不受外层代码控制,由处理器自行分配-C/C++可以建议分配方式,使用句柄(包含引用类型和引用地址)来操作数据。
1067 0
|
19天前
|
监控 Java
java异步判断线程池所有任务是否执行完
通过上述步骤,您可以在Java中实现异步判断线程池所有任务是否执行完毕。这种方法使用了 `CompletionService`来监控任务的完成情况,并通过一个独立线程异步检查所有任务的执行状态。这种设计不仅简洁高效,还能确保在大量任务处理时程序的稳定性和可维护性。希望本文能为您的开发工作提供实用的指导和帮助。
80 17

推荐镜像

更多