当前位置: 首页 > 后端技术 > Java

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

时间:2023-04-02 00:36:20 Java

全网最硬核的Java新内存模型分析与实验-五、JVM底层内存屏障源码分析,如有疏漏,欢迎大家批评指正。如果您在网上发现有人抄袭本文,请举报并积极向本github仓库提交issue。感谢大家的支持~本文参考了很多文章、文档和论文,但是这个东西确实比较复杂,本人水平有限,可能理解不到位,如有异议,请留言。本系列会持续更新,在这里结合大家的问题和错误疏漏,欢迎大家留言。喜欢单体版的请访问:全网最硬核的Java新内存模型分析与实验单体版(持续更新在QA)全网hardcoreJava新内存模型——一、什么是Java内存模型全网最hardcoreJava新内存模型分析与实验——二、最硬核Java新内存的Atom访问与分词分析与实验全网内存模型-3.HardCoreUnderstandingMemoryBarrier(CPU+Compiler)全网最难的Java新内存模型分析与实验-4.Java新内存访问方式与实验最难的分析与实验-全网核心Java新内存模型-五、JVM底层内存屏障源码分析JMM相关文档:JavaLanguageSpecificationChapter17TheJSR-133CookbookforCompilerWriters-DougLea'sUsingJDK9MemoryOrderModes-DougLea的内存障碍、CPU和内存模型相关:弱内存模型与强内存模型内存障碍:软件黑客的硬件视图当代ARM和x86架构的详细分析内存模型=指令重新排序+存储原子乱序执行x86CPU相关信息:x86wikiIntel?64andIA-32ArchitecturesSoftwareDeveloperManualsFormalSpecificationofthex86InstructionSetArchitectureARMCPU相关资料:ARMwikiaarch64Cortex-A710Specification各种一致性的理解:CoherenceandConsistencyAleskey大神的JMM解释:AlekseyShipil?v-不要误解Java内存模型(上)AlekseyShipil?v-不要误解Java内存模型(下)相信很多Java开发都会用到Java中的各种并发同步机制,比如volatile、synchronized和Lock等,很多人也看过JSRChapter17Threads和Locks(地址:https://docs.oracle.com/javase/specs/jls/se17/html/jls-17.html),包括同步、Wait/Notify、Sleep&Yield、内存模型等,有做了很多Specification解释。但我也相信大多数人都和我一样。初读时,有种看热闹的感觉。清楚的认识。同时,结合Hotspot的实现和Hotspot源码的解读,我们甚至会发现,由于javac的静态代码编译优化和C1、C2的JIT编译优化,最终的性能代码与我们从规范中理解的不同。代码的行为可能不太一致。而且,这种不一致导致我们去学习Java内存模型(JMM,JavaMemoryModel),了解Java内存模型的设计。如果我们想通过实际代码来尝试,结果是我们可能正确的理解是有偏差的,导致误会。自己也在不断的尝试理解Java内存模型,重读JLS和各位大神的分析。本系列将从阅读这些规格和分析以及jcstress所做的一些实验中整理出一些理解。希望对大家理解Java9之后的Java内存模型和API抽象有所帮助。不过,我还是要强调一下,内存模型的设计是从抽象一些设计开始的,不关心底层。涉及的事情很多。本人水平有限,可能理解不到位。我会尽力解释每一点。所有的论点和参考文献都被提出来了。请不要完全相信这里的所有观点。如有异议,请具体举例反驳并留言。8.底层JVM实现分析8.1.JVM中OrderAccess的定义JVM中有各种使用内存屏障的地方:实现Java的各种语法元素(volatile、final、synchronized等)实现JDK的各种API(VarHandle、Unsafe、Thread等)内存GC需要的barrier:因为要考虑GC多线程和应用线程(GC算法中称为Mutator)的工作方式,停止世界(Stop-the-world,STW)的方式是什么,或者一个并发对象referencebarrier:比如分代GC,复制算法,年轻代GC,我们一般将存活的对象从一个S区复制到另一个S区,如果复制过程中,我们不想停止世界(Stop-the-world,STW),但同时作为应用程序线程,那么我们需要内存屏障,例如;维护障碍:比如分区GC算法,我们需要维护跨区域的引用表和每个区域的使用情况表,比如CardTable。如果我们希望应用程序线程与GC线程同时修改访问,而不是停止世界,这也需要内存屏障。JIT也需要内存屏障:同样,无论是应用线程解释执行代码,还是执行JIT优化后的代码,这里也需要内存屏障。这些内存屏障,不同的CPU,不同的操作系统,底层需要不同的代码实现,统一接口设计为:源码地址:orderAccess.hpp不同的CPU,不同的操作系统实现不同,结合前面的CPUOutof-ordertable:下面看linux+x86的实现:源码地址:orderAccess_linux_x86.hpp对于x86,由于Load和Load、Load和Store、Store和Store都有一致性保证,所以只要不出现编译乱序,然后是StoreStore、LoadLoad、LoadStorebarriers,所以这里我们看到StoreStore、LoadLoad、LoadStorebarriers的实现只是一个编译器barrier。同时,我们在上一篇文章中分析过,acquire其实相当于在Load之后加上LoadLoad和LoadStorebarrier。对于x86,编译器屏障仍然是必需的。release我们在上一篇文章中也分析过。其实相当于在Store前面加上了LoadStore和StoreStore。对于x86,仍然需要编译器屏障。于是,我们就有了下表:再来看看我们经常使用的Linuxaarch64下的实现:源码地址:orderAccess_linux_aarch64.hpp上表中提到了ARM的CPULoadandLoad,LoadandStore,StoreandStore,BothStore和Load将乱序。JVM不直接使用aarch64的CPU指令,而是使用C++封装的内存屏障实现。C++封装和我们前面讲的简单CPU模型的内存屏障非常相似,即读内存屏障(__atomic_thread_fence(__ATOMIC_ACQUIRE))、写内存屏障(__atomic_thread_fence(__ATOMIC_RELEASE))、读写内存屏障(full内存屏障,__sync_synchronize())。acquire的作用是作为接收点解包,让后面的所有人都能看到包的内容。它类似于简单的CPU模型。其实就是阻塞等待invalidate队列处理完,保证CPU缓存没有脏数据。release的作用是将之前的更新作为一个launchpoint打包发送出去,类比简单的CPU模型。实际上,它是阻塞并等待存储缓冲区完全刷新到CPU缓存中。因此,acquire和release分别使用readmemorybarriers和writememorybarriers来实现。LoadLoad保证第一个Load先于第二个,所以其实就是在第一个Load之后加一个readmemorybarrier,阻塞等待invalidatequeue处理完;LoadStore也是一样,保证第一次Load先于第二次Store,只要处理完invalidate队列,当前CPU没有对应的脏数据,不需要等待storebuffer要清除的当前CPU。StoreStore保证了第一个Store在第二个之前,所以实际上它在第一次写入之后放了一个readmemorybarrier,阻塞等待storebuffer完全刷新到CPUcache中;对于StoreLoad,比较特殊,因为第二次Load需要看到Store的最新值,即update不能只去storebuffer,同时expiration不能存在invalidatequeue中未处理,所以需要一个读写内存屏障,即全屏障。8.2.volatile和finalmemorybarriers的源码下面以arm为例,看一下volatilememorybarrier插入的相关代码。其实我们可以通过跟踪iload的字节码看到如果load是volatile关键字或者final关键字修饰的字段会发生什么,istore可以看到store被volatile关键字或者final关键字修饰的情况。对于字段访问,JVM中也有快路径和慢路径之分。我们这里只看一下快速路径的代码:对应源码:源码地址:templateTable_arm.cpp微信搜索“我的编程喵”关注公众号,微信添加作者,每天扫一扫,轻松提升技术,并赢得各种优惠:我会经常发布各种框架官方社区的一些好消息视频资料并添加个人翻译字幕到以下地址(包括上面的公众号),欢迎关注:知乎:https://www.zhihu.com/people/...B站:https://space.bilibili.com/31...