夜雨聆风学习资料网

ARTICLE · 1242783

JMM学习总结-HotSpot源码学习

JMM学习总结-HotSpot源码学习
  • 一、HotSpot 内存屏障的整体架构
    • Java 程序与 JMM 语义
    • C2 中间表示与优化
    • 平台相关代码生成
    • 目标平台执行
  • 二、OrderAccess:HotSpot 运行时的内存顺序抽象
    • 2.1 四种基础屏障
    • 2.2 源码学习摘录
    • 2.3 为什么 x86 上的屏障有时没有独立机器指令?
  • 三、C2 内存访问:LoadNode、StoreNode 与内存依赖
    • 3.1 MemNode:内存访问的中间表示
    • 3.2 内存状态与别名分析
  • 四、C2 屏障节点:MemBarRelease、MemBarAcquire 与 MemBarVolatile
    • 4.1 关键节点及职责
    • 4.2 C2AccessFence:根据访问模式插入屏障
    • 4.3 MemBarNode 为什么能参与优化?
  • 五、从 Java volatile 到 C2 与 CPU:完整推导
  • 六、C2 中另一类重要屏障:对象初始化与安全发布
  • 七、系统工程师如何验证这些结论?
    • 7.1 源码追踪清单
    • 7.2 用 JIT 编译日志和汇编验证
  • 八、最终学习总结
  • 九、源码精读:把关键代码真正读透
    • 9.1 `OrderAccess` 中的 acquire/release 并不等于完整 fence
    • 9.2 C2 屏障插入和优化是两个阶段
    • 9.3 不要把 CPU 内存屏障与指令同步屏障混为一谈
  • 十、总结:建立完整的 HotSpot 屏障分析能力
  • 十一、进阶:屏障分析中最容易混淆的三个概念
    • 1. 编译器屏障不等于硬件屏障
    • 2. 硬件屏障不等于缓存刷新
    • 3. Java volatile 不等于 HotSpot 内部的 `volatile` 字段

前言

本文旨在记录近期研读Java源码的学习心得与疑难问题。由于个人理解水平有限,文中内容难免存在疏漏,恳请读者不吝指正。

HotSpot 源码学习总结:OrderAccess、C2 内存访问与屏障实现

从系统工程师的角度,分析 HotSpot 内存屏障,关键不是记住某条 CPU 指令,而是沿着一条完整链路理解:Java 内存模型定义约束,C2 编译器在中间表示中维护内存访问顺序,HotSpot 运行时与平台相关代码将这些约束映射到编译器屏障、CPU 指令及原子操作。

本文以 OpenJDK 主线源码为主要参考,重点分析:

  1. OrderAccess:HotSpot 运行时和本地代码使用的内存访问顺序抽象。
  2. C2 内存访问节点:LoadNode、StoreNode、MemNode 与内存依赖。
  3. C2 屏障节点:MemBarRelease、MemBarAcquire、MemBarVolatile、MemBarCPUOrder。
  4. 从 Java 的 volatile、Acquire/Release 访问一路追踪到编译器优化和 CPU 屏障。
  5. 如何从系统工程角度验证正确性、分析性能开销。

先明确一个重要边界:OrderAccess 与 C2 屏障节点不是同一个层次的实现。 前者主要服务于 HotSpot C++ 代码和运行时;后者参与 Java 方法的编译、优化和机器码生成。它们都受内存顺序约束,但不能直接等同。

一、HotSpot 内存屏障的整体架构

Java 程序与 JMM 语义

volatile、同步、VarHandle、原子操作

C2 中间表示与优化

Load / Store / MemNode / MemBarNode

内存依赖、别名分析、节点调度、屏障消除或合并

平台相关代码生成

Matcher、平台 AD 文件、架构相关实现

目标平台执行

普通 load/store、编译器约束、CPU fence、原子指令

这条链路不是每次都生成一条显式硬件屏障。C2 可以通过依赖关系约束优化器,也可以利用目标 CPU 已经提供的内存顺序保证,从而不生成额外的 fence 指令。

学习标记 H1: 源码中出现 MemBar,不代表最终机器码中一定出现一条独立的屏障指令。

学习标记 H2: 源码中没有显式的 mfence 或 dmb,也不代表没有内存顺序约束。约束可能由编译器 IR、普通访存指令的架构语义或其他原子指令共同实现。

参考源码:

  • OpenJDK orderAccess.hpp

  • OpenJDK barrierSetC2.cpp

  • OpenJDK memnode.cpp

二、OrderAccess:HotSpot 运行时的内存顺序抽象

核心源码:

  • src/hotspot/share/runtime/orderAccess.hpp
  • src/hotspot/os_cpu/linux_x86/orderAccess_linux_x86.hpp

OrderAccess 将平台相关的内存顺序操作抽象为统一接口。调用者不必在每处代码中直接编写 x86、AArch64 或 PPC 的汇编指令,而是表达需要的排序约束,再由平台实现负责兑现。

2.1 四种基础屏障

HotSpot 在 orderAccess.hpp 中定义了四类基础内存屏障:

屏障
主要约束
直观理解
loadload()
Load → Load
前面的读不能被重排到后面的读之后
storestore()
Store → Store
前面的写不能被重排到后面的写之后
loadstore()
Load → Store
前面的读不能被重排到后面的写之后
storeload()
Store → Load
前面的写不能被重排到后面的读之后

另外还有 acquire()、release() 和 fence() 等接口。完整语义应以当前版本的源码注释及平台实现为准。

2.2 源码学习摘录

下面是按源码接口整理的学习示意,不是完整源码复制:

// OrderAccess.hpp:核心接口的简化示意class OrderAccess {public:// [学习标记 OA1]// 约束两个读操作之间的顺序。static void loadload();// [学习标记 OA2]// 约束两个写操作之间的顺序。static void storestore();// [学习标记 OA3]// 约束读操作与后续写操作的顺序。static void loadstore();// [学习标记 OA4]// 约束写操作与后续读操作的顺序。static void storeload();// [学习标记 OA5]// 获取、发布与更强的双向屏障接口。static void acquire();static void release();static void fence();};

学习标记 OA6:屏障约束的是允许的内存访问顺序,不是简单地“刷新 CPU 缓存”。 缓存一致性、内存顺序和编译器重排是相关但不同的问题。缓存一致性本身不能保证所有跨变量访问都按照程序员期望的顺序发生。

2.3 为什么 x86 上的屏障有时没有独立机器指令?

观察 Linux x86 平台实现:

orderAccess_linux_x86.hpp 中使用了编译器屏障,并对 storeload() 和 fence() 采用更强的实现。源码的关键逻辑可以概括为:

// Linux x86 平台实现的简化摘录static inline void compiler_barrier() {// [学习标记 OA7]// 阻止 C++ 编译器将内存访问跨越此约束重排。    __asm__ volatile ("" : : : "memory");}inline void OrderAccess::loadload() {    compiler_barrier();}inline void OrderAccess::storestore() {    compiler_barrier();}inline void OrderAccess::loadstore() {    compiler_barrier();}inline void OrderAccess::storeload() {    fence();}inline void OrderAccess::fence() {// [学习标记 OA8]// 使用带锁的操作实现更强的硬件顺序约束。    __asm__ volatile ("lock; addl $0,0(%%rsp)"        : : : "cc", "memory"    );    compiler_barrier();}

以上是对应实现的简化摘录;具体汇编与实现细节应以目标分支文件为准。

这里有两个重要结论:

  1. x86 的 TSO 内存模型已经提供了相当强的硬件排序保证,因此某些屏障可以仅需编译器约束。
  2. Store → Load 是一个特别值得关注的场景,x86 上可能需要更强的硬件排序操作;源码中选择的具体实现也会考虑目标 CPU 的性能特征。

学习标记 OA9:不能把 x86 上的实现直接移植到弱内存序架构。 在 AArch64、PPC 等平台,某些相同的语义约束可能需要不同的屏障指令或带排序语义的访存指令。

还要注意,OrderAccess 是 HotSpot 内部的 C++ 实现抽象,并不是 Java 程序直接调用的公开 API。Java 的 volatile 语义由编译器、运行时和目标平台共同实现,不是简单地把每次访问都翻译成一次 OrderAccess::fence()。

三、C2 内存访问:LoadNode、StoreNode 与内存依赖

核心源码:

  • src/hotspot/share/opto/memnode.hpp
  • src/hotspot/share/opto/memnode.cpp
  • src/hotspot/share/gc/shared/c2/barrierSetC2.cpp

C2 编译器会将 Java 方法转换为基于节点的中间表示。内存访问不仅表现为一条 Load 或 Store,还带有控制、地址类型和内存状态等依赖信息。

3.1 MemNode:内存访问的中间表示

C2 的 MemNode 是多种内存访问节点的基础类型。它关注访问地址、地址类型以及该访问在内存依赖图中的位置。源码还通过别名分析识别不同访问可能涉及的内存区域。

简化示意:

// C2 内存访问节点的概念示意,不是完整类定义// [学习标记 C1]// Load:从地址读取值,并依赖相应的内存状态。LoadNode* load;// [学习标记 C2]// Store:向地址写入值,并产生新的内存状态。StoreNode* store;// [学习标记 C3]// 地址类型和别名分析帮助 C2 判断两个访问是否可能冲突。const TypePtr* adr_type;

例如:

int a = object.x;object.y = 10;

如果 C2 能够证明 x 和 y 是不同的内存位置,就可能允许某些优化;如果两个地址可能别名,编译器就必须保留必要的依赖。

学习标记 C4:内存依赖图不等于 CPU 内存屏障。 C2 可以只通过 IR 依赖限制优化器的重排,而最终机器码未必需要额外的硬件 fence。

3.2 内存状态与别名分析

C2 使用内存状态及别名信息判断访问之间的依赖关系。在编译器优化中,两个访问若已被证明互不影响,可能被重新排序、合并或消除;若存在冲突或同步要求,则必须保留相应约束。

系统工程师阅读源码时,可以重点追问:

  • 这个 Load 依赖哪个内存状态?
  • 这个 Store 更新了哪一部分内存状态?
  • 编译器能否证明两个地址不别名?
  • 该访问是否具有 volatile、Acquire、Release 或原子更新语义?
  • 屏障节点是约束优化器,还是还会影响最终机器码?

这些问题比单纯搜索 membar 更接近 C2 的实际工作方式。

四、C2 屏障节点:MemBarRelease、MemBarAcquire 与 MemBarVolatile

C2 的屏障逻辑可以从 barrierSetC2.cpp 中的 C2AccessFence 开始追踪。该类根据访问的装饰属性(例如读、写、原子访问、volatile、release、acquire)插入相应的屏障节点。

4.1 关键节点及职责

C2 节点
主要用途
学习重点
MemBarRelease
约束发布操作之前的内存访问
防止先前的写入被重排到发布写之后
MemBarAcquire
约束获取操作之后的内存访问
防止后续访问被重排到获取读之前
MemBarVolatile
表达 volatile 访问需要的内存顺序约束
维护 JMM 对 volatile 访问的要求
MemBarCPUOrder
约束特定访问的编译器及 CPU 顺序
用于特定内存访问场景,不能简单等同于完整的硬件 fence
MemBarStoreStore
表达 Store-Store 顺序约束
需要结合节点的实际使用与目标平台代码生成理解
InitializeNode
隔离对象分配后的初始化过程
防止关键初始化写入越过相关 safepoint 或对象发布

这些节点并不是可以随意互换的“屏障标签”。它们的具体语义由节点定义、C2 优化规则和平台代码生成共同决定。

4.2 C2AccessFence:根据访问模式插入屏障

下面是依据 OpenJDK 主线 C2AccessFence 逻辑整理的简化摘录:

// barrierSetC2.cpp// 仅保留用于学习内存顺序的关键逻辑。bool is_write   = (decorators & C2_WRITE_ACCESS) != 0;bool is_read    = (decorators & C2_READ_ACCESS) != 0;bool is_atomic  = is_read && is_write;bool is_volatile = (decorators & MO_SEQ_CST) != 0;bool is_release  = (decorators & MO_RELEASE) != 0;// [学习标记 C5]// 原子读改写操作需要正确约束其前后的内存访问。if (is_atomic) {if (is_release) {        kit->insert_mem_bar(Op_MemBarRelease);    } else if (is_volatile) {// 根据平台与内存模型要求选择相应的节点。        kit->insert_mem_bar(Op_MemBarVolatile);    }}// [学习标记 C6]// volatile 写或 release 写需要约束此前的访问。else if (is_write && (is_volatile || is_release)) {    kit->insert_mem_bar(Op_MemBarRelease);}// [学习标记 C7]// 特定场景下还需要额外的 CPU 顺序节点。if (access.needs_cpu_membar()) {    kit->insert_mem_bar(Op_MemBarCPUOrder);}

这是帮助理解源码结构的摘录,不是可直接替换原文件的完整代码。主线实现还包括平台能力判断、屏障节点配对、内存状态处理,以及访问完成后的 acquire/volatile 约束。

这里有三个需要重点理解的细节。

第一,C2 会区分访问的语义。 普通读写、volatile 访问、Acquire/Release 访问和原子读改写并不是同一种访问。C2 根据访问属性决定需要建立哪些依赖。

第二,屏障不仅是为了硬件。 OpenJDK 源码明确指出,一些屏障在多数机器上不会转换成实际的硬件屏障指令,但编译器仍需要遵守它们表达的顺序约束。

第三,插入屏障节点不代表最终保留原节点。 C2 后续优化可能消除、合并或改变某些节点,只要最终生成代码仍满足规定的内存语义。

4.3 MemBarNode 为什么能参与优化?

在 memnode.cpp 中,MemBarNode::make() 根据 opcode 创建相应的节点类型。MemBarNode::Ideal() 则包含优化逻辑,例如在特定条件下消除 volatile 屏障,或者消除不逃逸对象上的某些 release 屏障。

这说明 C2 屏障并非不可触碰的机器指令,而是参与编译器优化的中间表示。

学习标记 C8:屏障优化的正确性标准不是“屏障数量不能减少”,而是“优化后的程序仍然满足 Java 内存模型和其他运行时约束”。

五、从 Java volatile 到 C2 与 CPU:完整推导

考虑以下发布—获取代码:

class Publisher {int payload;volatile boolean ready;void publish(int value) {        payload = value;        ready = true;    }int consume() {if (ready) {return payload;        }return -1;    }}

生产者先写 payload,再写 volatile 标志;消费者读到相应的 volatile 写入后,再读取 payload。

从源码分析角度,可以把过程分成四步:

  1. Java 层:确定 JMM 约束

    payload = value 必须在发布 ready = true 的 volatile 写之前发生。消费者观察到相应的 volatile 写后,后续读取必须遵守获取端的顺序要求。

  2. C2 前端:创建内存访问节点

    payload 的写入、ready 的 volatile 写入、ready 的读取和 payload 的读取,分别被表示为相关内存访问节点,并带有相应的访问属性。

  3. C2 中间表示:建立屏障与依赖

    C2AccessFence 等逻辑根据访问属性建立必要的 release/acquire/volatile 约束,限制不允许的编译器重排。

  4. 平台代码生成:兑现剩余约束

    C2 根据目标平台生成相应的 load/store、原子指令或硬件屏障。对于已经由架构保证的排序,可能不需要额外的硬件 fence。

参考 C2AccessFence 的插入逻辑和 OrderAccess 的平台实现,可见编译器排序约束与硬件屏障是相互配合的两个层面。

学习标记 C9:不能根据 Java 源码中的一个 volatile 写,就断言机器码必定包含一条 mfence。 正确的分析方式是先确认 JMM 语义,再检查 C2 IR,最后检查具体 CPU 上的机器码。

六、C2 中另一类重要屏障:对象初始化与安全发布

除了 volatile 和 Acquire/Release,InitializeNode 也是阅读 C2 内存屏障实现时值得研究的节点。

memnode.cpp 对它的说明指出:该节点隔离 AllocateNode 之后、下一次可能发生的 safepoint 之前的对象初始化。它还允许收集和合并符合条件的初始化写入,并避免对某些内存区域进行冗余清零。

例如:

class Config {int port;boolean enabled;    Config() {        port = 8080;        enabled = true;    }}

在对象构造期间,C2 可以优化对象初始化,但必须维护对象分配、字段初始化和对象发布之间的必要约束。

这里需要区分两个概念:

  • InitializeNode 是 C2 对象初始化和内存依赖处理的一部分。
  • Java 对象的安全发布涉及 JMM 同步关系,不能只凭 InitializeNode 的存在,就推断任何发布方式都安全。

学习标记 C10:不要把所有名为 barrier 的节点都当作 CPU 内存屏障。 有些节点服务于对象初始化、GC、安全点或编译器内部依赖,其作用范围和语义并不相同。

七、系统工程师如何验证这些结论?

源码阅读是第一步。若需要确认某个 Java 并发操作在目标 JDK、目标 CPU 上究竟生成了什么代码,可以采用以下验证流程。

7.1 源码追踪清单

顺序
文件或模块
主要检查内容
1
runtime/orderAccess.hpp
抽象屏障的语义与接口
2
os_cpu/<平台>/orderAccess_<平台>.hpp
平台相关的编译器约束和硬件屏障
3
opto/memnode.hpp
 / memnode.cpp
Load、Store、MemBar 节点与优化
4
gc/shared/c2/barrierSetC2.cpp
访问属性如何触发 C2 屏障插入
5
平台 Matcher 与 AD 文件
节点如何匹配到目标指令
6
实际机器码
最终指令序列是否兑现所需语义

7.2 用 JIT 编译日志和汇编验证

可在测试程序中使用 JMH、JITWatch 或 hsdis 等工具观察 JIT 编译结果。例如,在支持相应诊断选项的 HotSpot 构建中,可以尝试:

java \  -XX:+UnlockDiagnosticVMOptions \  -XX:CompileCommand=print,Publisher::publish \  -XX:CompileCommand=print,Publisher::consume \  -XX:+PrintCompilation \  YourBenchmark

实际输出能力取决于 JDK 构建、反汇编器是否可用,以及方法是否真的由目标编译器编译。需要时,还可以使用 -XX:+LogCompilation 记录编译日志,配合 JITWatch 分析。

建议至少比较以下场景:

  • 普通字段读写与 volatile 字段读写。
  • VarHandle Plain、Acquire/Release 与 Volatile 访问。
  • x86-64 与 AArch64 上的生成代码。
  • 不同 JDK 版本和不同优化条件下的差异。

学习标记 H3:不要从单次汇编输出推导所有平台的实现。 必须记录 JDK 版本、CPU 架构、编译层级、JVM 参数和实际生成代码,才能使性能结论可复现。

八、最终学习总结

从系统工程角度,HotSpot 的内存屏障实现可以归纳为以下五条原则:

  1. OrderAccess 提供运行时层面的内存顺序抽象。 它隐藏平台差异,但不代表所有屏障都对应独立 CPU 指令。
  2. C2 使用内存访问节点与依赖图管理优化。 别名分析和内存状态帮助编译器判断哪些访问可以重排、合并或消除。
  3. MemBarNode 是编译器中间表示的一部分。 它既约束优化,也可能被后续阶段优化掉;正确性由最终生成代码是否保持语义决定。
  4. JMM 语义、编译器屏障与硬件屏障不能混为一谈。 Java 级别的同步约束,需要通过整个编译与运行时链路兑现。
  5. 性能必须在目标平台验证。 x86-64 与 AArch64 的内存模型不同,某一平台上的“无额外 fence”结论不能直接推广到其他平台。

推荐按 OrderAccess → MemNode → C2AccessFence → MemBarNode 优化 → 平台代码生成 → 实际机器码 的顺序学习。这样可以把 JMM 的抽象规则与 HotSpot 的具体实现联系起来,而不至于停留在“屏障就是一条 CPU 指令”的误区。

九、源码精读:把关键代码真正读透

最后建议重点研究三个源码细节,它们能帮助避免学习 HotSpot 时最常见的误区。

9.1 OrderAccess 中的 acquire/release 并不等于完整 fence

在当前 OpenJDK 主线的 orderAccess.hpp 注释中,acquire/release 被描述为围绕同步读写对的单向、异步屏障。源码建议在能够使用绑定式操作时优先使用 load_acquire()、release_store() 等形式,因为独立的 acquire()、release() 不知道具体哪个访问构成同步点,平台实现可能更保守。

9.2 C2 屏障插入和优化是两个阶段

C2AccessFence 根据访问装饰属性插入屏障节点,而 MemBarNode::Ideal() 等优化逻辑可能进一步简化节点。源码注释也明确说明,一些内存屏障不一定会在大多数机器上变成实际屏障指令,但仍必须约束编译器排序。

学习标记 C11: 调试时至少观察三个阶段:解析生成的 IR、优化后的 IR、最终机器码。只观察其中一个阶段,很容易错误判断屏障是否存在。

9.3 不要把 CPU 内存屏障与指令同步屏障混为一谈

OrderAccess 还提供 cross_modify_fence(),并在源码中区分普通内存访问排序与指令取指同步相关的操作。指令同步屏障主要服务于修改代码或其他需要保证后续指令取指看到正确内容的场景,并非普通 Java volatile 读写的通用替代品。

学习标记 C12: 研究屏障时先确认它约束的是数据内存访问、编译器重排、原子操作,还是指令流同步。名字中都带有 fence,不代表语义相同。

十、总结:建立完整的 HotSpot 屏障分析能力

从系统工程角度,建议将本专题的学习目标归纳为以下清单:

  • 能解释 LoadLoad、StoreStore、LoadStore、StoreLoad 的约束方向。
  • 能从 orderAccess.hpp 跟踪到目标架构的实现。
  • 能解释 C2 的 Load、Store、内存依赖与别名分析。
  • 能跟踪 C2AccessFence 如何根据访问属性插入屏障节点。
  • 能解释为什么某些 MemBar 节点最终不生成独立的硬件屏障。
  • 能对照最终机器码验证 x86-64 和 AArch64 的实现差异。
  • 能区分数据内存排序、对象初始化约束和指令同步屏障。

最终形成的分析路径应该是:

JMM 语义 → HotSpot 访问抽象 → C2 内存依赖与屏障节点 → 节点优化 → 平台指令选择 → 实际机器码验证。

掌握这条路径,就能从系统工程视角解释 HotSpot 为什么需要某个屏障、屏障如何约束优化、何时能被消除,以及它在特定处理器上究竟带来什么成本。

十一、进阶:屏障分析中最容易混淆的三个概念

1. 编译器屏障不等于硬件屏障

Linux x86 HotSpot 的 compiler_barrier() 使用带有 "memory" clobber 的内联汇编。它约束 C++ 编译器的内存优化,但自身不发出 CPU 内存屏障指令。

static inline void compiler_barrier(){// [学习标记 X1]// 阻止编译器跨越该位置任意重排相关内存访问。// 不等于发出 mfence 或其他硬件 fence。__asm__ volatile ("" : : : "memory");}

2. 硬件屏障不等于缓存刷新

内存屏障的主要作用是约束内存访问顺序,不是要求 CPU 把全部缓存内容刷新到 DRAM。缓存一致性协议和内存排序模型是两个不同的机制。

学习标记 X2: 分析屏障开销时,应关注指令的排序效果、缓存行竞争、流水线约束及原子操作的实现成本,而不是简单认为“屏障就是刷新缓存”。

3. Java volatile 不等于 HotSpot 内部的 volatile 字段

HotSpot C++ 中的 volatile 关键字、Java 语言中的 volatile 修饰符以及 C2 的 MemBarVolatile 节点,是三个不同层次的概念。

  • C++ volatile 不是通用的跨线程同步原语。
  • Java volatile 的语义由 JMM 规定。
  • C2 MemBarVolatile 是编译器中间表示中的节点,最终如何实现取决于优化与平台代码生成。

最终学习目标: 能够从 JMM 的正确性约束出发,追踪 OrderAccess、C2 内存依赖和屏障节点,再通过实际生成的机器码验证实现。这样才能准确解释 HotSpot 的内存屏障为什么存在、何时可以省略,以及它在特定硬件上的真实成本。

相关学习资料