ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

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

【JVM原理详解】41-JMM基础-主内存与工作内存 41-JMM基础-主内存与工作内存引言前几个模块我们一直在讲JVM的内部世界——类加载、运行时数据区、垃圾回收、JIT编译。但从本篇开始视角要切换到另一个维度多线程下内存如何表现。一个线程对变量的写入另一个线程什么时候能看见看似简单的可见性问题底层牵涉到CPU缓存、指令重排序、内存屏障等一整套机制。这套机制的抽象规范就是Java Memory ModelJMM。JMM是理解volatile、synchronized、happens-before的根基。不弄懂JMM并发编程就只能停留在背规则层面遇到诡异bug便无从下手。本篇从JMM的定义出发讲清主内存与工作内存的划分、8种原子操作、内存屏障分类以及JMM与CPU多级缓存的映射关系。什么是Java内存模型Java Memory ModelJMM是Java语言规范JLS §17.4定义的一套抽象模型用于描述多线程环境下变量共享内存的访问规则。它规定了线程之间如何通过内存进行交互——一个线程对共享变量的写入何时、以何种顺序对其他线程可见。需要强调几个关键点JMM是规范不是实现。它定义了应该怎样HotSpot等JVM负责落地实现。不同JVM只要遵守JMM规范并发语义就一致。JMM针对的是共享变量即存储在堆中的实例字段、静态字段以及数组元素。局部变量和方法参数是线程私有的不存在内存可见性问题JMM不约束它们。JMM的核心目标是正确性在正确性前提下尽量给编译器和CPU留出优化空间。这是一个安全与性能的平衡。JMM在JDK 5JSR-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个操作必须满足以下规则read与load配对不允许read了却不load也不允许load了却不read。即主内存读出的值必须载入工作内存。store与write配对不允许store了却不write也不允许write了却不store。即工作内存存出的值必须写入主内存。assign与store可间隔assign之后不一定要立刻store可以攒在工作内存里。lock与unlock成对一个变量同一时刻只能被一条线程locklock几次就要unlock几次。lock会清空工作内存中该变量的副本unlock前必须把变量同步回主内存。一个赋值操作的完整流程以x 1这条赋值语句为例假设变量x是共享变量线程执行时大致经历read从主内存读取x的当前值load载入到工作内存的变量副本use把副本值传给执行引擎如果需要旧值的话assign执行引擎把新值1赋给工作内存的变量副本store把新值从工作内存传送到主内存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读后插入LoadLoad和LoadStore——保证volatile读之后的普通读/写不重排到volatile读之前synchronized的unlock隐含StoreLoad语义——释放锁前的写必须对后续获得锁的线程可见final字段写在构造函数返回前插入StoreStore——保证构造完成时final字段已初始化并可见StoreLoad是全能屏障同时具备LoadLoad、LoadStore、StoreStore的效果因为它要求Store完全生效后才能Load。在x86上StoreLoad对应mfence或lock前缀指令开销较大数十到上百周期所以JVM会尽量避免不必要的StoreLoad。与物理内存的关系JMM是抽象模型但它的落地离不开硬件。理解JMM与物理内存的映射能帮助你看清volatile、synchronized的底层成本。CPU的多级缓存现代CPU都有多级缓存以Intel架构为例CPU核心0 CPU核心1 ┌──────────┐ ┌──────────┐ │ 寄存器 │ │ 寄存器 │ ← 最快纳秒级 ├──────────┤ ├──────────┤ │ L1 Cache │ │ L1 Cache │ ← 私有~1ns32KB │ (L1d/L1i)│ │ (L1d/L1i)│ ├──────────┤ ├──────────┤ │ L2 Cache │ │ L2 Cache │ ← 私有~4ns256KB~1MB ├──────────┴────┴──────────┤ │ L3 Cache │ ← 共享~12ns几MB~几十MB ├───────────────────────────┤ │ 主内存 (DRAM) │ ← ~100nsGB级 └───────────────────────────┘L1/L2是每个核心私有的这就是工作内存的物理基础之一——一个核心修改了L1的值另一个核心看不到L3多核共享但仍有缓存一致性延迟寄存器是编译器优化的重灾区变量可能长期只在寄存器里流转根本不写回缓存缓存一致性与MESI多核CPU通过缓存一致性协议Cache Coherence Protocol保证各核心看到一致的内存视图。最主流的是MESI协议它给每个缓存行定义4种状态MModified该缓存行被修改与主内存不一致本核心独占EExclusive该缓存行与主内存一致本核心独占SShared该缓存行与主内存一致多个核心共享IInvalid该缓存行无效当一个核心修改了处于S状态的缓存行MESI会通过总线消息把其他核心的副本置为I失效强制它们下次读时从主内存或修改方核心重新加载。这正是JMM工作内存失效的硬件落地。JMM与硬件的映射JMM抽象硬件对应主内存DRAM物理内存工作内存CPU寄存器 L1/L2缓存 编译器优化位置read/load从DRAM或L3加载到L1/寄存器store/write从寄存器/L1刷到L3或DRAMlock总线锁或缓存锁MESI的M状态内存屏障mfence/lfence/sfence/lock前缀指令需要强调JMM的抽象是为了跨平台一致性。不同CPU架构的内存模型强弱不同——x86是强有序模型TSOTotal Store Order很多重排序天然不会发生而ARM/POWER是弱内存模型需要插入更多屏障。JVM的职责是在不同硬件上补齐屏障让Java程序看到一致的JMM语义。这就是为什么同样一段无volatile的并发代码在x86上可能碰巧正确换到ARM上就出错——JMM保证了你在加上正确的同步原语后两个平台都正确。代码示例观察工作内存延迟下面用一个示例直观感受工作内存与主内存的延迟同步问题。// 适用 JDK 11/17publicclassVisibilityDemo{// 注意没加 volatile下面程序可能永远不会终止privatestaticbooleanrunningtrue;publicstaticvoidmain(String[]args)throwsInterruptedException{ThreadworkernewThread(()-{longcount0;// worker线程读自己的工作内存副本主线程的修改对它可能不可见while(running){count;}System.out.println(Worker stopped, countcount);});worker.start();Thread.sleep(1000);runningfalse;// 主线程修改但未必同步到worker的工作内存worker.join(2000);System.out.println(Main exit, worker aliveworker.isAlive());}}运行这段代码很多时候程序会正常在1秒后停止但在某些JVM/CPU组合下尤其-server模式、JIT优化后worker线程会永远循环下去——因为JIT把running优化到了寄存器worker根本不去主内存刷新。把running改为volatile后强制每次读都从主内存加载严格说是通过屏障保证可见性程序必然正常停止。这就是JMM在工作内存层面的直接体现下一篇我们会深入volatile的语义。实践要点JMM是抽象模型不要硬套到实现。8种操作是规范描述HotSpot实际不会按6步执行赋值。理解抽象是为了读懂规则不是为了逐条对应字节码。工作内存的副本不一定是独立的内存拷贝。它可能是寄存器里的一份值也可能是缓存行里的一份甚至JIT优化后压根没有副本这个实体只是对某线程暂时不可见的中间状态。把工作内存理解为线程可见性边界更准确。跨平台一致性是JMM的核心价值。x86的强内存模型掩盖了很多并发bug但代码部署到ARM如Apple Silicon、AWS Graviton就会暴露。永远用JMM规则而非x86上跑对了作为正确性判据。StoreLoad开销最大。volatile写、解锁操作会触发StoreLoad这是volatile比普通变量慢一个数量级的主因。高频写场景慎用volatile。缓存行是缓存一致性的最小单位。即使只有一个boolean变量MESI也是按缓存行通常64字节失效的。多个变量挤在同一缓存行会导致伪共享False Sharing下一篇之后讲并发底层时会展开。JMM不约束局部变量。方法内的局部变量、参数都是线程私有的不存在可见性问题也不受JMM约束。不要把虚拟机栈里的局部变量表和工作内存混为一谈——后者专指共享变量的线程私有视图。小结JMM是Java语言规范定义的抽象模型规定多线程下共享变量的访问规则核心是可见性与有序性JMM把内存划分为主内存共享变量权威存储与工作内存线程私有副本线程只能操作工作内存通信必须经主内存8种原子操作lock/unlock/read/load/use/assign/store/write描述了线程与内存的交互必须满足配对与同步规则内存屏障LoadLoad/StoreStore/LoadStore/StoreLoad是保证有序性、可见性的底层机制StoreLoad开销最大JMM与硬件的映射工作内存≈寄存器L1/L2缓存编译器优化主内存≈DRAMlock≈MESI缓存锁或总线锁下一篇我们将基于这套抽象深入原子性、可见性、有序性三大特性并剖析指令重排序与经典的i和双重检查锁定问题。
返回列表