CC 咖啡猫的工作空间 Coding Space
  1. 冯·诺依曼架构
  • 核心思想:程序和数据都存储在内存中,CPU 按顺序从内存取指令、执行。
  • 五大部件:运算器、控制器、存储器、输入设备、输出设备。
  • 冯·诺依曼瓶颈:CPU 处理速度远高于内存访问速度,CPU 经常等待数据(后续的缓存、流水线、预取等技术都是为了缓解这个瓶颈)
  • 理解意义:软件可以被当作数据保存、复制和修改——这就是为什么操作系统能加载程序、编译器能生成可执行文件。
  • 内存里如何区分"程序"和"数据"?
    • 核心答案:不区分,全是二进制。同一个内存地址,这一刻被当作指令执行,下一刻可能被当作数据读取。
    • CPU 靠"身份"来切换:
      • PC(程序计数器)指向谁,谁就是指令——CPU 进入"取指模式"。
      • LOAD/STORE 指令指向谁,谁就是数据——CPU 进入"读/写数据模式"。
    • 从 Java 代码到机器指令的完整链路:
      .java 源码 → javac 编译 → .class 字节码 → JVM 加载
        → 解释执行 / JIT 编译 → 机器码(二进制指令)→ CPU 逐条执行
      
    • 具体例子:int total = price + tax; 编译后变成类似这样的机器指令存在内存中:
      0x1000: LOAD  R1, [0x2000]    // 读 price(取数据)
      0x1004: LOAD  R2, [0x2004]    // 读 tax(取数据)
      0x1008: ADD   R3, R1, R2      // 加法(执行计算)
      0x100C: STORE [0x2008], R3    // 写回 total(存数据)
      
      这 4 条机器指令和被操作的变量值全在同一个内存里,CPU 在"取指阶段"把它们当指令解析,在"访存阶段"把它们当数据读写。
  1. CPU 内部结构
  • CPU 由多个功能单元协作:
    功能单元 作用
    ALU(算术逻辑单元) 执行算术和逻辑运算
    CU(控制单元) 解析指令,发出控制信号
    寄存器组 保存正在计算的临时数据
    PC(程序计数器) 保存下一条指令的地址
    IR(指令寄存器) 保存当前正在执行的指令
  • 寄存器 vs 内存:
    • 寄存器在 CPU 内部,速度极快(1 个时钟周期),容量极小(几十到几百个)。
    • 内存在 CPU 外部,速度相对慢(~100ns),容量大得多。
    • Java 关联:JIT 编译器会尽可能把热点变量分配到寄存器,减少内存访问。
  1. 指令执行流程(取指 → 译码 → 执行 → 访存 → 写回)
  • 取指 Fetch:根据 PC 从内存/缓存读取下一条指令。
  • 译码 Decode:控制单元解析操作码和操作数。
  • 执行 Execute:ALU 完成计算。
  • 访存 Memory:如果指令需要读写内存,则访问缓存或主存。
  • 写回 Write Back:把结果写回寄存器。
  • 例如 int total = price + tax; 底层会被拆成:
    1. 把 price 读入寄存器
    2. 把 tax 读入寄存器
    3. 执行加法
    4. 把结果写回 total 对应的内存位置
  1. 指令集架构(ISA:Instruction Set Architecture)
  • ISA 是软件和 CPU 硬件的接口/契约,定义了 CPU 支持哪些指令、指令格式、寻址方式等。
  • 一条指令通常包含:
    • 操作码 Opcode:告诉 CPU 做什么(加、减、加载、跳转)。
    • 操作数 Operand:告诉 CPU 对谁操作(寄存器、内存地址、立即数)。
  • 寻址方式:
    寻址方式 示例 特点
    立即数寻址 MOV R1, #100 数据直接写在指令里,最快
    寄存器寻址 ADD R1, R2 数据在寄存器里,速度快
    直接寻址 MOV R1, [0x1000] 指令中包含内存地址
    间接寻址 MOV R1, [R2] 寄存器里保存内存地址
  • CISC(x86) vs RISC(ARM、RISC-V):
    • CISC:指令多且复杂,长度不固定,单条指令能力强,流水线优化较难(x86 生态庞大)。
    • RISC:指令少且规整,长度固定,流水线友好,功耗低(ARM 统治移动端,Apple Silicon 证明了 RISC 也能高性能)。
  1. 存储层次(为什么需要分层)
  • 目标:用得起的价格,获得接近最快存储的速度。
  • 层次结构(从上到下,速度递减,容量递增,成本递减):
    寄存器(1 周期,~KB)
      → L1 Cache(~1ns,~64KB)
      → L2 Cache(~4ns,~256KB)
      → L3 Cache(~12ns,~8MB+)
      → 内存(~100ns,~GB)
      → SSD/硬盘(~100µs-10ms,~TB)
    
  • Java 关联:对象在堆上分配(内存),频繁访问的字段可能被加载到 CPU Cache,栈上变量更可能分配到寄存器。
  1. CPU Cache 专题(高频八股 + Java 关联) 6.1 局部性原理(缓存有效的原因)
    • 时间局部性:刚访问过的数据,短时间内可能再次访问。
    • 空间局部性:访问某个地址后,附近地址也可能很快被访问。
    • Java 关联:数组连续存储,遍历 int[] 比遍历 LinkedList<Integer> 缓存友好得多。

6.2 Cache Line(缓存行) - CPU 缓存以固定大小的块(缓存行,通常 64 字节)为单位存取数据,不是逐字节操作。 - Java 关联——伪共享(False Sharing): - 两个线程分别修改两个独立变量,但这两个变量恰好在同一个缓存行里,会导致缓存行在 CPU 之间反复失效和同步,性能严重下降。 - 解决方案: - JDK 8+:@sun.misc.Contended 注解(需加 JVM 参数 -XX:-RestrictContended)。 - 手动填充:在变量前后加 64 字节的 padding 字段。 - 典型场景:LongAdder 内部的 Cell 类就用 @Contended 避免了伪共享。

6.3 缓存映射方式 - 直接映射:每个内存块只能映射到唯一的缓存行,简单但容易冲突。 - 全相联映射:可以映射到任意缓存行,灵活但硬件复杂度高。 - 组相联映射(主流):折中方案,缓存分成多组,每组内有多个路(way),如 8-way 组相联。

6.4 缓存写策略 - Write-through(写直达):同时写缓存和内存,简单但慢。 - Write-back(写回):只写缓存,缓存行被淘汰时才写回内存,性能好但实现复杂。 - Write-allocate / No-write-allocate:写缺失时是否先将数据加载到缓存。

6.5 缓存一致性协议(MESI) - 多核 CPU 各自有独立的 L1/L2 缓存,需要保证同一内存地址在各核心缓存中数据一致。 - MESI 四种状态: | 状态 | 含义 | |------|------| | M(Modified) | 已修改,数据仅在本核心缓存中,与内存不一致 | | E(Exclusive) | 数据仅在本核心缓存中,与内存一致 | | S(Shared) | 数据在多个核心缓存中,均与内存一致 | | I(Invalid) | 缓存行无效 | - Java 关联——volatile: - volatile 变量的写操作会触发 CPU 的 Store-Load 屏障,将当前核心缓存的修改写回内存,并使其他核心的缓存行失效(MESI 中的 Invalidate),从而保证可见性。 - 这也是为什么 volatile 有性能开销——它涉及缓存一致性协议的通信。

6.6 缓存替换算法 - LRU(最近最少使用):淘汰最久未访问的缓存行。 - PLRU(伪 LRU):LRU 的近似实现,硬件成本更低。 - 随机替换:实现最简单。

  1. CPU 缓存与内存屏障(→ JMM 内存模型)
  • 现代 CPU 为了性能会做指令重排(Out-of-Order Execution),编译器和 CPU 都可能打乱代码执行顺序。
  • 内存屏障(Memory Barrier / Memory Fence)是强制约束指令顺序的硬件指令。
  • 四种基本屏障:
    屏障类型 作用
    LoadLoad 保证前面的读操作先于后面的读操作完成
    StoreStore 保证前面的写操作先于后面的写操作对其他核心可见
    LoadStore 保证前面的读操作先于后面的写操作完成
    StoreLoad 保证前面的写操作对其他核心可见后,再进行后面的读操作(最强,开销最大)
  • Java 关联——JMM(Java Memory Model)
    • JMM 的 happens-before 规则本质上是编译器/JVM 向代码中插入适当的内存屏障。
    • volatile 写:前面插入 StoreStore,后面插入 StoreLoad。
    • volatile 读:前面插入 LoadLoad,后面插入 LoadStore。
    • synchronized:进入时获取锁(acquire 语义),退出时释放锁(release 语义),对应 LoadStore/StoreStore 屏障。
    • final:构造函数末尾插入 StoreStore 屏障,保证 final 字段初始化完成后才被其他线程看到。
  1. 指令流水线与分支预测
  • 流水线(Pipeline):多条指令同时在 CPU 的不同阶段执行,提升吞吐量而非单条指令速度。 顺序执行:指令1(IF→ID→EX→MEM→WB) → 指令2(IF→ID→EX→MEM→WB) 流水线: 指令1(IF→ID→EX→MEM→WB) 指令2( IF→ID→EX→MEM→WB) 指令3( IF→ID→EX→MEM→WB)
  • 流水线冒险(Hazard):
    冒险类型 含义 处理方式
    结构冒险 多条指令争用同一硬件 增加硬件资源
    数据冒险 后指令依赖前指令结果 数据转发、插入气泡
    控制冒险 分支跳转改变执行流 分支预测
  • 分支预测:CPU 猜测 if 跳转方向,猜对省时间,猜错清空流水线重来。
  • Java 关联:排好序的数组在大量 if 判断时比无序数组快很多,就是因为分支预测命中率高。
    // 排序后更快——分支预测友好
    Arrays.sort(data);
    for (int x : data) {
        if (x > threshold) count++;
    }
    
  1. 总线与 IO
  • 总线分类:
    总线 作用
    地址总线 CPU 指定要访问哪个地址
    数据总线 CPU、内存、设备之间传输数据
    控制总线 传输读写、中断、时钟等控制信号
  • IO 访问方式:
    方式 特点
    程序查询(轮询) CPU 反复查询设备状态,浪费 CPU
    中断 设备完成后通知 CPU,有中断处理开销
    DMA(直接内存访问) 设备直接和内存交换数据,CPU 不必逐字节搬运
  • Java 关联:NIO 的 DirectByteBuffer 利用 DMA 减少了数据在 JVM 堆内存和 OS 内存之间的拷贝次数。FileChannel.transferTo() 在底层也会尝试使用 DMA / sendfile 零拷贝技术。
  1. 常见面试问题
  • 冯·诺依曼架构的核心思想是什么?瓶颈是什么?
  • 存储层次为什么分层?寄存器、Cache、内存、硬盘的速度量级是多少?
  • 什么是缓存行?什么是伪共享?Java 中如何解决?
  • MESI 协议的四种状态分别是什么?和 volatile 有什么关系?
  • volatile 底层是如何通过内存屏障保证可见性和有序性的?
  • 为什么排好序的数组遍历时 if 判断更快?(分支预测)
  • CPU 流水线的三种冒险分别是什么?
  • 内存屏障有哪几种?synchronized、volatile、final 分别对应什么屏障?
  1. 学习建议
  • 不需要会设计 CPU,但需要理解"代码跑在硬件上的真实模型"。
  • 重点掌握:存储层次 → CPU Cache → 缓存行/伪共享 → MESI → 内存屏障 → volatile/JMM。这是一条从硬件到 Java 的完整链路。
  • 性能优化时先有证据(profiling),不要凭感觉猜。大多数业务代码的可读性比微小的缓存优化更重要。