当前位置: 首页 > news >正文

【JVM原理详解】41-JMM基础-主内存与工作内存

41-JMM基础-主内存与工作内存

引言

前几个模块我们一直在讲JVM的"内部世界"——类加载、运行时数据区、垃圾回收、JIT编译。但从本篇开始,视角要切换到另一个维度:多线程下内存如何表现。一个线程对变量的写入,另一个线程什么时候能看见?看似简单的可见性问题,底层牵涉到CPU缓存、指令重排序、内存屏障等一整套机制。这套机制的抽象规范,就是Java Memory Model(JMM)

JMM是理解volatile、synchronized、happens-before的根基。不弄懂JMM,并发编程就只能停留在"背规则"层面,遇到诡异bug便无从下手。本篇从JMM的定义出发,讲清主内存与工作内存的划分、8种原子操作、内存屏障分类,以及JMM与CPU多级缓存的映射关系。

什么是Java内存模型

Java Memory Model(JMM)是Java语言规范(JLS §17.4)定义的一套抽象模型,用于描述多线程环境下变量(共享内存)的访问规则。它规定了线程之间如何通过内存进行交互——一个线程对共享变量的写入,何时、以何种顺序对其他线程可见。

需要强调几个关键点:

  • JMM是规范,不是实现。它定义了"应该怎样",HotSpot等JVM负责落地实现。不同JVM只要遵守JMM规范,并发语义就一致。
  • JMM针对的是共享变量,即存储在堆中的实例字段、静态字段,以及数组元素。局部变量和方法参数是线程私有的,不存在内存可见性问题,JMM不约束它们。
  • JMM的核心目标是正确性,在正确性前提下尽量给编译器和CPU留出优化空间。这是一个"安全与性能"的平衡。

JMM在JDK 5(JSR-133)经历了一次重要重构,重新定义了volatile和final的语义,奠定了现代Java并发的基础。JDK 8/11/17的JMM核心语义与JSR-133保持一致,后续主要是增强工具(如JDK 9引入的VarHandle提供更细粒度的内存访问模式)。

主内存与工作内存

JMM规定了内存的两层划分:主内存(Main Memory)工作内存(Working Memory,又称本地内存/Local Memory)

线程A 线程B 线程C ┌──────┐ ┌──────┐ ┌──────┐ │ 工作内存 │ │ 工作内存 │ │ 工作内存 │ │ (本地副本)│ │ (本地副本)│ │ (本地副本)│ └───┬──┘ └───┬──┘ └───┬──┘ │ │ │ └──────────┬───────┴──────────┬───────┘ │ │ ▼ ▼ ┌─────────────────────────────┐ │ 主内存 (Main Memory) │ │ 共享变量: 实例字段 / 静态字段 / 数组元素 │ └─────────────────────────────┘

主内存

主内存是所有共享变量的"权威存储"。所有共享变量都存在于主内存中,它是线程间通信的"中转站"。可以把主内存粗略对应到物理机的RAM(但JMM是抽象模型,不严格等于硬件RAM)。

工作内存

工作内存是每个线程私有的内存区域,保存了该线程读写的共享变量的副本。线程对变量的所有操作(读、写)都在工作内存中进行,不能直接读写主内存中的变量。不同线程之间无法访问对方的工作内存,线程间变量值的传递必须通过主内存完成。

工作内存对应到物理层面,包括:

  • CPU的寄存器
  • L1/L2/L3缓存
  • 编译器优化产生的"内存位置"(有时变量压根不进内存,只在寄存器里流转)

注意:JMM的工作内存不等于JVM运行时数据区中的"虚拟机栈"。虚拟机栈存的是局部变量和操作数栈,而工作内存存的是共享变量的副本,二者是不同维度的概念。一个共享变量在某线程工作内存里有副本,同时该线程的虚拟机栈里可能还有它的临时拷贝(操作数栈上的值)。

8种原子操作

JMM定义了8种操作来描述线程与主内存之间的交互。每个操作都是原子的——要么完整执行,要么不执行,不会被其他线程打断。

操作作用对象含义
lock(锁定)主内存变量把一个变量标识为一条线程独占状态
unlock(解锁)主内存变量把一个处于锁定状态的变量释放出来,释放后其他线程可锁定
read(读取)主内存变量把一个变量的值从主内存传输到线程的工作内存中,供后续load使用
load(载入)工作内存变量把read操作从主内存得到的值放入工作内存的变量副本中
use(使用)工作内存变量把工作内存中一个变量的值传递给执行引擎,每当虚拟机遇到需要使用变量的字节码指令时执行
assign(赋值)工作内存变量把一个从执行引擎接收到的值赋给工作内存的变量,每当虚拟机遇到给变量赋值的字节码指令时执行
store(存储)工作内存变量把工作内存中一个变量的值传送到主内存中,供后续write使用
write(写入)主内存变量把store操作从工作内存得到的值放入主内存的变量中

这8个操作的协作关系如下图:

主内存 (Main Memory) ┌───────────────────────────┐ │ 变量 x = ... │ │ ▲ │ │ │ write │ │ │ ▲ │ │ │ │ store (工作→主) │ │ │ │ │ │ │ │ read (主→工作) │ │ │ ▼ │ │ │load │ │ ▼ │ └───┼───────────────────────┘ │ ▼ ┌───────────────────────────┐ │ 工作内存 (Working Memory) │ │ 变量副本 x = ... │ │ ▲ │ │ │ assign (引擎→副本) │ │ │ use (副本→引擎) │ │ ▼ │ │ 执行引擎 (Execution Engine) │ └───────────────────────────┘

操作的配对规则

JMM规定这8个操作必须满足以下规则:

  1. read与load配对:不允许read了却不load,也不允许load了却不read。即主内存读出的值必须载入工作内存。
  2. store与write配对:不允许store了却不write,也不允许write了却不store。即工作内存存出的值必须写入主内存。
  3. assign与store可间隔:assign之后不一定要立刻store,可以攒在工作内存里。
  4. lock与unlock成对:一个变量同一时刻只能被一条线程lock,lock几次就要unlock几次。lock会清空工作内存中该变量的副本,unlock前必须把变量同步回主内存。

一个赋值操作的完整流程

x = 1这条赋值语句为例,假设变量x是共享变量,线程执行时大致经历:

  1. read:从主内存读取x的当前值
  2. load:载入到工作内存的变量副本
  3. use:把副本值传给执行引擎(如果需要旧值的话)
  4. assign:执行引擎把新值1赋给工作内存的变量副本
  5. store:把新值从工作内存传送到主内存
  6. write:把新值写入主内存的x

注意这只是逻辑描述,实际HotSpot并不会真的分6步执行——JIT和CPU会做大量优化(如合并、重排序)。JMM的8种操作是规范层面的抽象,不是实现的指令序列。

内存屏障

8种原子操作描述了"做什么",但**内存屏障(Memory Barrier,又称内存栅栏)**描述了"如何保证可见性与有序性"。内存屏障是CPU和编译器层面的机制,用于禁止特定类型的指令重排序,并强制刷新缓存。

JMM涉及4种内存屏障:

屏障类型指令含义作用
LoadLoadLoad1; LoadLoad; Load2保证Load1先于Load2完成,禁止Load2及之后的读重排到Load1之前
StoreStoreStore1; StoreStore; Store2保证Store1先于Store2完成(且对其他处理器可见),禁止Store2重排到Store1之前
LoadStoreLoad1; LoadStore; Store2保证Load1先于Store2完成,禁止Store2重排到Load1之前
StoreLoadStore1; StoreLoad; Load2保证Store1对其他处理器可见后,才执行Load2。开销最大,因为要等Store完全刷出

屏障与8种操作的对应

JMM的8种原子操作并不直接对应到硬件屏障,但可以粗略映射:

  • volatile写前插入StoreStore,写后插入StoreLoad——保证写前的普通写不重排到volatile写之后,且volatile写对其他线程立即可见
  • volatile读后插入LoadLoadLoadStore——保证volatile读之后的普通读/写不重排到volatile读之前
  • synchronized的unlock隐含StoreLoad语义——释放锁前的写必须对后续获得锁的线程可见
  • final字段写在构造函数返回前插入StoreStore——保证构造完成时final字段已初始化并可见

StoreLoad是全能屏障,同时具备LoadLoad、LoadStore、StoreStore的效果,因为它要求Store完全生效后才能Load。在x86上StoreLoad对应mfencelock前缀指令,开销较大(数十到上百周期),所以JVM会尽量避免不必要的StoreLoad。

与物理内存的关系

JMM是抽象模型,但它的落地离不开硬件。理解JMM与物理内存的映射,能帮助你看清volatile、synchronized的底层成本。

CPU的多级缓存

现代CPU都有多级缓存,以Intel架构为例:

CPU核心0 CPU核心1 ┌──────────┐ ┌──────────┐ │ 寄存器 │ │ 寄存器 │ ← 最快,纳秒级 ├──────────┤ ├──────────┤ │ L1 Cache │ │ L1 Cache │ ← 私有,~1ns,32KB │ (L1d/L1i)│ │ (L1d/L1i)│ ├──────────┤ ├──────────┤ │ L2 Cache │ │ L2 Cache │ ← 私有,~4ns,256KB~1MB ├──────────┴────┴──────────┤ │ L3 Cache │ ← 共享,~12ns,几MB~几十MB ├───────────────────────────┤ │ 主内存 (DRAM) │ ← ~100ns,GB级 └───────────────────────────┘
  • L1/L2是每个核心私有的,这就是"工作内存"的物理基础之一——一个核心修改了L1的值,另一个核心看不到
  • L3多核共享,但仍有缓存一致性延迟
  • 寄存器是编译器优化的"重灾区",变量可能长期只在寄存器里流转,根本不写回缓存

缓存一致性与MESI

多核CPU通过缓存一致性协议(Cache Coherence Protocol)保证各核心看到一致的内存视图。最主流的是MESI协议,它给每个缓存行定义4种状态:

  • M(Modified):该缓存行被修改,与主内存不一致,本核心独占
  • E(Exclusive):该缓存行与主内存一致,本核心独占
  • S(Shared):该缓存行与主内存一致,多个核心共享
  • I(Invalid):该缓存行无效

当一个核心修改了处于S状态的缓存行,MESI会通过总线消息把其他核心的副本置为I(失效),强制它们下次读时从主内存或修改方核心重新加载。这正是JMM"工作内存失效"的硬件落地

JMM与硬件的映射

JMM抽象硬件对应
主内存DRAM(物理内存)
工作内存CPU寄存器 + L1/L2缓存 + 编译器优化位置
read/load从DRAM或L3加载到L1/寄存器
store/write从寄存器/L1刷到L3或DRAM
lock总线锁或缓存锁(MESI的M状态)
内存屏障mfence/lfence/sfence/lock前缀指令

需要强调:JMM的抽象是为了跨平台一致性。不同CPU架构的内存模型强弱不同——x86是强有序模型(TSO,Total Store Order),很多重排序天然不会发生;而ARM/POWER是弱内存模型,需要插入更多屏障。JVM的职责是在不同硬件上"补齐"屏障,让Java程序看到一致的JMM语义。这就是为什么同样一段无volatile的并发代码,在x86上可能"碰巧正确",换到ARM上就出错——JMM保证了你在加上正确的同步原语后,两个平台都正确。

代码示例:观察工作内存延迟

下面用一个示例直观感受工作内存与主内存的延迟同步问题。

// 适用 JDK 11/17publicclassVisibilityDemo{// 注意:没加 volatile,下面程序可能永远不会终止privatestaticbooleanrunning=true;publicstaticvoidmain(String[]args)throwsInterruptedException{Threadworker=newThread(()->{longcount=0;// worker线程读自己的工作内存副本,主线程的修改对它可能不可见while(running){count++;}System.out.println("Worker stopped, count="+count);});worker.start();Thread.sleep(1000);running=false;// 主线程修改,但未必同步到worker的工作内存worker.join(2000);System.out.println("Main exit, worker alive="+worker.isAlive());}}

运行这段代码,很多时候程序会正常在1秒后停止;但在某些JVM/CPU组合下(尤其-server模式、JIT优化后),worker线程会永远循环下去——因为JIT把running优化到了寄存器,worker根本不去主内存刷新。

running改为volatile后,强制每次读都从主内存加载(严格说是通过屏障保证可见性),程序必然正常停止。这就是JMM在工作内存层面的直接体现,下一篇我们会深入volatile的语义。

实践要点

  1. JMM是抽象模型,不要硬套到实现。8种操作是规范描述,HotSpot实际不会按6步执行赋值。理解抽象是为了读懂规则,不是为了逐条对应字节码。

  2. 工作内存的"副本"不一定是独立的内存拷贝。它可能是寄存器里的一份值,也可能是缓存行里的一份,甚至JIT优化后压根没有"副本"这个实体,只是"对某线程暂时不可见的中间状态"。把工作内存理解为"线程可见性边界"更准确。

  3. 跨平台一致性是JMM的核心价值。x86的强内存模型掩盖了很多并发bug,但代码部署到ARM(如Apple Silicon、AWS Graviton)就会暴露。永远用JMM规则(而非"x86上跑对了")作为正确性判据。

  4. StoreLoad开销最大。volatile写、解锁操作会触发StoreLoad,这是volatile比普通变量慢一个数量级的主因。高频写场景慎用volatile。

  5. 缓存行是缓存一致性的最小单位。即使只有一个boolean变量,MESI也是按缓存行(通常64字节)失效的。多个变量挤在同一缓存行会导致"伪共享"(False Sharing),下一篇之后讲并发底层时会展开。

  6. JMM不约束局部变量。方法内的局部变量、参数都是线程私有的,不存在可见性问题,也不受JMM约束。不要把虚拟机栈里的局部变量表和"工作内存"混为一谈——后者专指共享变量的线程私有视图。

小结

  • JMM是Java语言规范定义的抽象模型,规定多线程下共享变量的访问规则,核心是可见性与有序性
  • JMM把内存划分为主内存(共享变量权威存储)与工作内存(线程私有副本),线程只能操作工作内存,通信必须经主内存
  • 8种原子操作(lock/unlock/read/load/use/assign/store/write)描述了线程与内存的交互,必须满足配对与同步规则
  • 内存屏障(LoadLoad/StoreStore/LoadStore/StoreLoad)是保证有序性、可见性的底层机制,StoreLoad开销最大
  • JMM与硬件的映射:工作内存≈寄存器+L1/L2缓存+编译器优化,主内存≈DRAM,lock≈MESI缓存锁或总线锁

下一篇我们将基于这套抽象,深入原子性、可见性、有序性三大特性,并剖析指令重排序与经典的i++和双重检查锁定问题。

http://www.jsqmd.com/news/1349399/

相关文章:

  • 2026 年更新:云南靠谱的花岗岩石栏杆制造厂家哪家可靠,花几十万装的景区栏杆,竟不是真石材?别再被坑了! - 实业推荐官
  • 高频信号注入法:电机电感参数辨识原理与工程实践
  • 从Arduino到STM32:IIC通信、PID控制与FreeRTOS的嵌入式项目实战
  • 开源项目第178期:OptMem — 426 个 token 的提示词,给 AI Agent 跨会话的持久记忆
  • 2026年跨境卖家必备:TRO发起代理机构哪家好?盘点合规靠谱服务商、避坑指南FAQ及优质机构实力解析 - 行业观察网
  • Krokiet终极指南:简单快速清理重复文件的免费开源神器
  • PDF文件怎么压缩?2026实测多种方法对比,在线工具推荐一览 - 工具软件使用方法推荐
  • Minecraft 1.21终极指南:5分钟搞定MASA全家桶中文汉化
  • ESP32开发板选型与配置:从“无限重启”到稳定运行的底层原理与实战
  • 2026亚马逊链接投诉机构选型攻略:正规合规服务商筛选要点、避坑FAQ及**机构推荐 - 行业观察网
  • Dev-C++ 6.3 安装、卸载与中文乱码终极解决方案
  • PDF怎么转PNG?4种实用转换方法及2026在线工具对比 - 工具软件使用方法推荐
  • Word怎么转换成图片?快捷键+5种方法实测,2026最全转换指南 - 工具软件使用方法推荐
  • MIPI CSI链路计算:从信号衰减到PCB设计的实战指南
  • 本科毕业论文写作,这些实用工具能帮你少走弯路
  • 3分钟永久解锁IDM:免费激活Internet Download Manager完整指南
  • 如何在Blender中一键规整UV网格:UvSquares插件完整教程
  • 光伏与工程废旧电缆科普:回收价值怎么算?优质废旧电缆回收推荐标准 - 品牌测评网
  • 2026亚马逊链接侵权投诉服务商全盘点 合规实力测评 适配场景选型避坑全指南FAQ - 商业大观
  • Mermaid Live Editor终极指南:5个实用技巧快速创建专业图表
  • PyTorch模型训练5大避坑指南:解决显存泄漏与设备不匹配报错
  • Mermaid Live Editor:免费在线图表编辑器的终极使用指南 [特殊字符]
  • 从BERT、GPT到GLM:大语言模型核心架构对比与实战选型指南
  • PPT怎么转PDF?2026实测5种操作方法 - 工具软件使用方法推荐
  • 2026年8月云南省昆明市电信单宽带小白避坑指南 - 找卡家园
  • Unity URP渲染管线中模板与深度测试实现秘境空间效果
  • G-Helper终极指南:告别华硕Armoury Crate臃肿软件,轻量化控制您的笔记本
  • 2026年服装厂别急着上系统:先想清楚这几件事,生产管理才能落地
  • ABAQUS计算中断问题诊断与解决:从日志解读到模型优化全攻略
  • Unity VR开发:解决CharacterController蹲下碰撞体不同步问题