JMM深度解析(一) 计算机硬件存储体系

0 阅读9分钟

Java内存模型(JMM)深度解析

计算机硬件存储体系与JMM基础

引言:为什么需要JMM?

因为有这么多级的缓存(CPU和物理主内存的速度不一致),CPU的运行并不是直接操作内存而是先把内存里边的数据读到缓存,而内存的读和写操作的时候就会造成不一致的问题。

JVM规范中试图定义一种Java内存模型(Java Memory Model,简称JMM)来屏蔽掉各种硬件和操作系统的内存访问差异,以实现让Java程序在各种平台下都能达到一致的内存访问效果。

1. 计算机硬件存储层次结构

现代计算机系统采用多层次的存储体系来平衡性能和成本,从CPU到主内存形成了一个复杂的存储层次结构。

1.1 完整的硬件协同工作时序图

以下时序图展示了CPU寄存器、Store/Load Buffer、多级缓存、缓存行、总线和内存一致性协议如何协同完成一次完整的内存访问操作:

可见性读.png

可见性写.png 时序图关键要点说明:

  1. 多级缓存查找:CPU按照寄存器→Load Buffer→L1→L2→L3→总线→内存的层次顺序查找数据
  2. 缓存行操作:所有缓存操作都以64字节缓存行为单位进行
  3. MESI协议控制:确保多核环境下的缓存一致性
  4. 异步优化:Store Buffer和Load Buffer提供异步处理能力
  5. 性能分层:越靠近CPU的存储层次访问速度越快
  6. 一致性代价:共享状态的写操作需要广播失效,性能开销较大
1.2 存储层次结构图
graph TD
    A[CPU核心] --> B[寄存器组]
    A --> C[Store Buffer]
    A --> D[Load Buffer]
    A --> E[L1缓存]
    E --> F[L2缓存]
    F --> G[L3缓存]
    G --> H[主内存RAM]
    
    subgraph "CPU内部"
        B
        C
        D
    end
    
    subgraph "缓存层次"
        E
        F
        G
    end
    
    style A fill:#ff9999
    style B fill:#ffcc99
    style C fill:#ffcc99
    style D fill:#ffcc99
    style E fill:#99ccff
    style F fill:#99ccff
    style G fill:#99ccff
    style H fill:#99ff99

2. 各存储层次的特性分析

2.1 CPU寄存器

物理特性:

  • 容量:x86-64架构有16个通用寄存器,每个64位
  • 访问速度:1个时钟周期(约0.3纳秒)
  • 位置:CPU核心内部,每个核心独有
  • 可见性:仅对当前CPU核心可见

寄存器的缓存特性: 虽然寄存器本质上是指令执行的临时存储,但通过编译器和CPU的优化,会形成事实上的"缓存"效应:

  • 编译器优化:将频繁访问的变量分配到寄存器中长期驻留
  • CPU流水线重用:循环中复用寄存器值,避免重复内存访问
  • 寄存器重命名:物理寄存器池动态映射,延长数据生命周期

工作原理:

sequenceDiagram
    participant CPU as CPU核心
    participant REG as 寄存器
    participant L1 as L1缓存
    
    CPU->>REG: 1. 加载操作数
    CPU->>REG: 2. 执行运算
    REG->>CPU: 3. 返回结果
    Note over REG: 数据可能长期驻留
    CPU->>L1: 4. 条件性写回

缓存机制: 虽然寄存器本质上是临时存储,但编译器优化会导致数据长期驻留:

// 示例:循环变量优化
for (int i = 0; i < 1000000; i++) {
    sum += array[i];
    // 变量i可能全程存储在寄存器中,不写回内存
}

对应的汇编优化:

mov rcx, 0          ; i存入寄存器rcx
loop_start:
    add rax, [array+rcx*4]  ; 直接使用寄存器值
    inc rcx                 ; 寄存器内自增
    cmp rcx, 1000000
    jl loop_start
2.2 Store Buffer(存储缓冲区)

Store Buffer是现代CPU中至关重要的硬件组件,位于CPU核心与缓存之间,用于优化写操作性能,避免CPU因等待缓存一致性协议确认而阻塞。

诞生原因: 当CPU执行写操作时,如果缓存行处于Shared(S)状态(其他CPU可能持有副本),需要通过MESI协议广播Invalidate消息使其他缓存失效,并等待所有CPU确认。等待期间(约数百时钟周期),CPU只能空转,严重浪费算力。

Store Buffer的核心特性:

graph LR
    A[Store Buffer特性] --> B[容量小]
    A --> C[异步处理]
    A --> D[私有性]
    A --> E[性能优化]
    
    B --> B1[通常32-64个条目]
    B --> B2[每个条目存储地址+数据]
    
    C --> C1[写操作立即返回]
    C --> C2[后台处理缓存一致性]
    C --> C3[避免CPU阻塞]
    
    D --> D1[每个CPU核心独有]
    D --> D2[其他核心不可见]
    D --> D3[导致可见性延迟]
    
    E --> E1[提升写操作吞吐量]
    E --> E2[隐藏缓存一致性延迟]
    
    style A fill:#ff9999
    style B fill:#99ccff
    style C fill:#ffcc99
    style D fill:#ff6666
    style E fill:#99ff99

Store Buffer的工作流程:

sequenceDiagram
    participant CPU as CPU核心
    participant SB as Store Buffer
    participant L1 as L1缓存
    participant Bus as 系统总线
    participant Other as 其他CPU
    
    Note over CPU: 执行写操作
    CPU->>SB: 1. 数据存入Store Buffer
    Note over CPU: CPU立即继续执行
    
    par 异步处理
        SB->>Bus: 2. 发送Invalidate消息
        Bus->>Other: 3. 通知其他CPU失效缓存
        Other->>Bus: 4. 返回ACK确认
        Bus->>SB: 5. 收到所有ACK
        SB->>L1: 6. 数据提交到L1缓存
    end
    
    Note over SB,L1: 其他CPU此时才能看到数据变化

Store Buffer导致的问题:

  1. 可见性延迟:写操作完成后,数据可能仍在Store Buffer中,其他CPU无法立即看到
  2. 指令重排序:Store Buffer可能改变内存操作的执行顺序
  3. 读取自己的写入:CPU会优先从Store Buffer读取数据,可能读到未提交的值

实际影响示例:

// 线程1
int a = 1;     // 可能停留在Store Buffer
boolean flag = true;  // 可能先于a写入内存

// 线程2
if (flag) {
    int x = a;  // 可能读到a的旧值0,而不是1
}

c

2.3 L1/L2/L3缓存层次与缓存行机制

缓存行(Cache Line)基础概念:

缓存行是CPU缓存系统的基本存储和传输单位,理解缓存行机制对于理解JMM的底层实现至关重要。

  • 定义:缓存行是CPU缓存中数据传输和存储的最小单位
  • 大小:通常为64字节(x86-64架构)
  • 对齐:内存地址按缓存行大小对齐
  • 原子性:整个缓存行作为一个整体进行操作

缓存行的结构:

graph LR
    subgraph "缓存行结构(64字节)"
        A[Tag标签] --> B[数据块0-7]
        A --> C[数据块8-15]
        A --> D[数据块16-23]
        A --> E[数据块24-31]
        A --> F[数据块32-39]
        A --> G[数据块40-47]
        A --> H[数据块48-55]
        A --> I[数据块56-63]
    end
    
    subgraph "MESI状态"
        J[Modified]
        K[Exclusive]
        L[Shared]
        M[Invalid]
    end
    
    A --> J
    
    style A fill:#ff9999
    style J fill:#ffcc99
    style K fill:#99ccff
    style L fill:#99ff99
    style M fill:#cccccc

缓存特性对比:

缓存级别容量访问延迟位置共享范围缓存行数量缓存行大小
L1数据缓存32KB3-4周期CPU核心内单核私有512行64字节
L1指令缓存32KB3-4周期CPU核心内单核私有512行64字节
L2缓存256KB-1MB10-20周期CPU核心内单核私有4K-16K行64字节
L3缓存8MB-32MB30-70周期CPU芯片内多核共享128K-512K行64字节

缓存行在内存访问流程中的作用:

  1. 数据加载:CPU访问内存时,以缓存行为单位加载数据
  2. 空间局部性:一次加载64字节,提高相邻数据访问效率
  3. 一致性维护:MESI协议以缓存行为单位维护一致性
  4. 写回策略:脏缓存行统一写回主内存

缓存行的性能影响:

场景缓存行状态性能影响说明
独占读写Exclusive/Modified最优无需总线通信
多核只读Shared良好读操作无需通信
多核读写Shared→Modified较差需要失效其他副本
缓存未命中Invalid最差需要从内存加载

伪共享问题: 当多个线程访问同一缓存行的不同变量时,会导致缓存行在CPU间频繁传递:

// 伪共享示例
class FalseSharing {
    volatile long a;  // 可能在同一缓存行
    volatile long b;  // 可能在同一缓存行
    
    // 线程1频繁修改a,线程2频繁修改b
    // 导致缓存行在CPU间频繁失效和传递
}

// 解决方案:缓存行填充
class NoPadding {
    volatile long a;
    long p1, p2, p3, p4, p5, p6, p7; // 填充56字节
    volatile long b;
}

3. 缓存一致性协议详解

3.1 MESI协议详解

MESI协议是最常用的缓存一致性协议,通过四种状态来维护多核CPU之间的缓存一致性。理解MESI协议对于理解Java内存模型的底层实现至关重要。

四种缓存行状态

graph LR
    A[MESI协议状态] --> B[Modified - M]
    A --> C[Exclusive - E]
    A --> D[Shared - S]
    A --> E[Invalid - I]
    
    B --> B1[已修改]
    B --> B2[与主内存不一致]
    B --> B3[只有当前CPU持有]
    B --> B4[负责写回主内存]
    
    C --> C1[独占]
    C --> C2[与主内存一致]
    C --> C3[只有当前CPU持有]
    C --> C4[可直接修改为M状态]
    
    D --> D1[共享]
    D --> D2[与主内存一致]
    D --> D3[多个CPU可能持有]
    D --> D4[写入前需广播失效]
    
    E --> E1[无效]
    E --> E2[缓存行不包含有效数据]
    E --> E3[需要从其他地方加载]
    
    style A fill:#ff9999
    style B fill:#ff6666
    style C fill:#99ccff
    style D fill:#ffcc99
    style E fill:#cccccc

MESI状态转换图

stateDiagram-v2
    [*] --> Invalid
    Invalid --> Exclusive: 独占读取(无其他副本)
    Invalid --> Shared: 共享读取(存在其他副本)
    Exclusive --> Modified: 本地写入
    Exclusive --> Shared: 其他CPU读取
    Shared --> Modified: 本地写入(广播失效)
    Shared --> Invalid: 其他CPU写入
    Modified --> Shared: 其他CPU读取(写回内存)
    Modified --> Invalid: 其他CPU写入(写回内存)
    
    Invalid: I - 缓存行无效\n需要重新加载
    Exclusive: E - 独占且干净\n与内存一致
    Shared: S - 共享且干净\n与内存一致
    Modified: M - 独占且脏\n与内存不一致

普通写操作在不同状态下的行为

缓存行状态写操作行为是否广播通知性能影响
Modified (M)直接修改缓存❌ 否最快
Exclusive (E)直接修改缓存,状态变为M❌ 否很快
Shared (S)广播Invalidate,等待ACK,状态变为M✅ 是较慢
Invalid (I)先获取缓存行,再修改间接触发最慢

关键洞察

  • 静默写入:当缓存行处于M或E状态时,写操作无需通知其他CPU
  • 广播失效:只有S状态的写操作需要广播Invalidate消息
  • 异步优化:Store Buffer使得S状态的写操作也能快速返回
3.2 MESI协议消息类型

总线消息:

  • Read:请求读取缓存行
  • Read Response:返回缓存行数据
  • Invalidate:使其他CPU的缓存行失效
  • Invalidate Acknowledge:确认缓存行已失效
  • Read Invalidate:读取并使其他CPU缓存行失效
  • Writeback:将修改的缓存行写回内存

协议交互示例:

sequenceDiagram
    participant CPU0 as CPU0缓存
    participant CPU1 as CPU1缓存
    participant BUS as 总线
    participant MEM as 主内存
    
    Note over CPU0,CPU1: 初始状态:都是Invalid
    
    CPU0->>BUS: Read X
    BUS->>MEM: 读取内存
    MEM->>BUS: 返回数据
    BUS->>CPU0: 数据
    Note over CPU0: 状态:Exclusive
    
    CPU1->>BUS: Read X
    BUS->>CPU0: 共享请求
    CPU0->>BUS: 提供数据
    Note over CPU0: 状态:Shared
    Note over CPU1: 状态:Shared
    
    CPU0->>BUS: Write X
    BUS->>CPU1: Invalidate
    CPU1->>BUS: Ack
    Note over CPU0: 状态:Modified
    Note over CPU1: 状态:Invalid

4. 硬件层面的协同工作机制

4.1 写操作的完整流程
flowchart TD
    A[CPU执行写指令] --> B[检查Store Buffer]
    B --> C{Store Buffer满?}
    C -->|否| D[写入Store Buffer]
    C -->|是| E[等待Store Buffer排空]
    E --> D
    D --> F[检查缓存行状态]
    F --> G{MESI状态}
    G -->|M/E| H[直接修改缓存行]
    G -->|S| I[发送Invalidate消息]
    G -->|I| J[缓存未命中处理]
    I --> K[等待其他CPU确认]
    K --> L[修改缓存行状态为M]
    J --> M[从内存/其他缓存加载]
    M --> L
    H --> N[写操作完成]
    L --> N
    
    style D fill:#ffcc99
    style H fill:#99ccff
    style L fill:#99ccff
4.2 读操作的完整流程
flowchart TD
    A[CPU执行读指令] --> B[检查Store Buffer]
    B --> C{Store Buffer命中?}
    C -->|是| D[返回Store Buffer数据]
    C -->|否| E[检查L1缓存]
    E --> F{L1命中?}
    F -->|是| G[返回L1数据]
    F -->|否| H[检查L2缓存]
    H --> I{L2命中?}
    I -->|是| J[加载到L1并返回]
    I -->|否| K[检查L3缓存]
    K --> L{L3命中?}
    L -->|是| M[加载到L2/L1并返回]
    L -->|否| N[发送总线读请求]
    N --> O{其他CPU有Modified?}
    O -->|是| P[从其他CPU获取最新数据]
    O -->|否| Q[从主内存读取]
    P --> R[更新缓存层次]
    Q --> R
    R --> S[返回数据]
    
    style D fill:#ffcc99
    style G fill:#99ff99
    style J fill:#99ff99
    style M fill:#99ff99
    style S fill:#99ff99

读操作的关键点

  1. Store Buffer优先级:CPU首先检查自己的Store Buffer,确保读取到自己最新写入的值
  2. 缓存层次查找:按L1→L2→L3的顺序查找,利用局部性原理
  3. 缓存一致性:当所有缓存都未命中时,通过总线协议确保获取最新数据

5. 可见性问题的根源

5.1 多层缓存导致的可见性延迟
// 线程1在CPU0上运行
class VisibilityProblem {
    private int value = 0;
    
    // 线程1:写操作
    public void writer() {
        value = 42;  // 可能只更新CPU0的缓存
    }
    
    // 线程2:读操作(在CPU1上运行)
    public void reader() {
        int local = value;  // 可能读到旧值0
    }
}

问题分析:

sequenceDiagram
    participant T1 as 线程1(CPU0)
    participant C0 as CPU0缓存
    participant C1 as CPU1缓存
    participant T2 as 线程2(CPU1)
    participant MEM as 主内存
    
    T1->>C0: value = 42
    Note over C0: 缓存行状态:Modified
    Note over MEM: 主内存仍为0
    
    T2->>C1: 读取value
    C1->>MEM: 缓存未命中,读主内存
    MEM->>C1: 返回0(旧值)
    C1->>T2: 返回0
    
    Note over T2: 线程2看到旧值!
5.2 Store Buffer导致的写入延迟
// Dekker算法的失效示例
class StoreBufferProblem {
    volatile boolean flag1 = false;
    volatile boolean flag2 = false;
    
    // 线程1
    void thread1() {
        flag1 = true;        // 可能在Store Buffer中
        if (!flag2) {        // 可能读到旧值
            // 临界区
        }
    }
    
    // 线程2
    void thread2() {
        flag2 = true;        // 可能在Store Buffer中
        if (!flag1) {        // 可能读到旧值
            // 临界区 - 两个线程可能同时进入!
        }
    }
}

6. 指令重排序的硬件基础

6.1 CPU流水线与乱序执行
graph LR
    subgraph "指令流水线"
        A[取指] --> B[译码]
        B --> C[执行]
        C --> D[访存]
        D --> E[写回]
    end
    
    subgraph "乱序执行单元"
        F[指令队列]
        G[保留站]
        H[执行单元1]
        I[执行单元2]
        J[重排序缓冲区]
    end
    
    E --> F
    F --> G
    G --> H
    G --> I
    H --> J
    I --> J

重排序类型:

  1. 编译器重排序:编译时指令优化
  2. 处理器重排序:CPU执行时的动态调度
  3. 内存系统重排序:Store Buffer等硬件优化
6.2 内存访问重排序示例
// 可能被重排序的代码
class ReorderingExample {
    int a = 0;
    boolean flag = false;
    
    void writer() {
        a = 1;           // 写操作1
        flag = true;     // 写操作2
        // CPU可能重排序为:flag = true; a = 1;
    }
    
    void reader() {
        if (flag) {      // 读操作1
            assert a == 1; // 读操作2 - 可能失败!
        }
    }
}

7. JMM如何解决硬件问题

7.1 JMM的抽象模型
graph TB
    subgraph "线程1"
        T1[线程1]
        WM1[工作内存1]
        T1 --> WM1
    end
    
    subgraph "线程2"
        T2[线程2]
        WM2[工作内存2]
        T2 --> WM2
    end
    
    subgraph "主内存"
        MM[共享变量]
    end
    
    WM1 -.->|read/load| MM
    MM -.->|store/write| WM1
    WM2 -.->|read/load| MM
    MM -.->|store/write| WM2
    
    style WM1 fill:#ffcc99
    style WM2 fill:#ffcc99
    style MM fill:#99ff99

工作内存的物理对应:

  • CPU寄存器
  • CPU缓存(L1/L2/L3)
  • Store Buffer / Load Buffer
  • 编译器优化的临时存储
7.2 JMM的内存操作

JMM定义了8种原子操作来描述变量在主内存和工作内存之间的交互

sequenceDiagram
    participant T as 线程
    participant WM as 工作内存
    participant MM as 主内存
    
    Note over T,MM: 读操作序列
    MM->>WM: read(从主内存读取)
    WM->>WM: load(加载到工作内存)
    WM->>T: use(线程使用变量)
    
    Note over T,MM: 写操作序列
    T->>WM: assign(线程赋值)
    WM->>WM: store(准备写回主内存)
    WM->>MM: write(写入主内存)
    
    Note over T,MM: 同步操作
    MM->>WM: lock(获取锁)
    WM->>MM: unlock(释放锁)

8种原子操作的详细说明

操作作用域描述
read主内存把变量值从主内存传输到工作内存
load工作内存把read操作得到的值放入工作内存的变量副本中
use工作内存把工作内存中的变量值传递给执行引擎
assign工作内存把执行引擎接收到的值赋给工作内存的变量
store工作内存把工作内存中的变量值传送到主内存
write主内存把store操作得到的值放入主内存的变量中
lock主内存把变量标识为线程独占状态
unlock主内存释放变量的独占状态

小结: 这里详细介绍了计算机硬件存储体系的各个层次、缓存行的详细机制、它们的协同工作机制。这为理解Java内存模型提供了完整的理论基础。