- 冯·诺依曼架构
- 核心思想:程序和数据都存储在内存中,CPU 按顺序从内存取指令、执行。
- 五大部件:运算器、控制器、存储器、输入设备、输出设备。
- 冯·诺依曼瓶颈:CPU 处理速度远高于内存访问速度,CPU 经常等待数据(后续的缓存、流水线、预取等技术都是为了缓解这个瓶颈)。
- 理解意义:软件可以被当作数据保存、复制和修改——这就是为什么操作系统能加载程序、编译器能生成可执行文件。
- 内存里如何区分"程序"和"数据"?
- 核心答案:不区分,全是二进制。同一个内存地址,这一刻被当作指令执行,下一刻可能被当作数据读取。
- CPU 靠"身份"来切换:
- PC(程序计数器)指向谁,谁就是指令——CPU 进入"取指模式"。
- LOAD/STORE 指令指向谁,谁就是数据——CPU 进入"读/写数据模式"。
- 从 Java 代码到机器指令的完整链路:
.java 源码 → javac 编译 → .class 字节码 → JVM 加载 → 解释执行 / JIT 编译 → 机器码(二进制指令)→ CPU 逐条执行 - 具体例子:
int total = price + tax;编译后变成类似这样的机器指令存在内存中:
这 4 条机器指令和被操作的变量值全在同一个内存里,CPU 在"取指阶段"把它们当指令解析,在"访存阶段"把它们当数据读写。0x1000: LOAD R1, [0x2000] // 读 price(取数据) 0x1004: LOAD R2, [0x2004] // 读 tax(取数据) 0x1008: ADD R3, R1, R2 // 加法(执行计算) 0x100C: STORE [0x2008], R3 // 写回 total(存数据)
- CPU 内部结构
- CPU 由多个功能单元协作:
功能单元 作用 ALU(算术逻辑单元) 执行算术和逻辑运算 CU(控制单元) 解析指令,发出控制信号 寄存器组 保存正在计算的临时数据 PC(程序计数器) 保存下一条指令的地址 IR(指令寄存器) 保存当前正在执行的指令 - 寄存器 vs 内存:
- 寄存器在 CPU 内部,速度极快(1 个时钟周期),容量极小(几十到几百个)。
- 内存在 CPU 外部,速度相对慢(~100ns),容量大得多。
- Java 关联:JIT 编译器会尽可能把热点变量分配到寄存器,减少内存访问。
- 指令执行流程(取指 → 译码 → 执行 → 访存 → 写回)
- 取指 Fetch:根据 PC 从内存/缓存读取下一条指令。
- 译码 Decode:控制单元解析操作码和操作数。
- 执行 Execute:ALU 完成计算。
- 访存 Memory:如果指令需要读写内存,则访问缓存或主存。
- 写回 Write Back:把结果写回寄存器。
- 例如
int total = price + tax;底层会被拆成:- 把 price 读入寄存器
- 把 tax 读入寄存器
- 执行加法
- 把结果写回 total 对应的内存位置
- 指令集架构(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 周期,~KB) → L1 Cache(~1ns,~64KB) → L2 Cache(~4ns,~256KB) → L3 Cache(~12ns,~8MB+) → 内存(~100ns,~GB) → SSD/硬盘(~100µs-10ms,~TB) - Java 关联:对象在堆上分配(内存),频繁访问的字段可能被加载到 CPU Cache,栈上变量更可能分配到寄存器。
- 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 的近似实现,硬件成本更低。 - 随机替换:实现最简单。
- 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 字段初始化完成后才被其他线程看到。
- 指令流水线与分支预测
- 流水线(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++; }
- 总线与 IO
- 总线分类:
总线 作用 地址总线 CPU 指定要访问哪个地址 数据总线 CPU、内存、设备之间传输数据 控制总线 传输读写、中断、时钟等控制信号 - IO 访问方式:
方式 特点 程序查询(轮询) CPU 反复查询设备状态,浪费 CPU 中断 设备完成后通知 CPU,有中断处理开销 DMA(直接内存访问) 设备直接和内存交换数据,CPU 不必逐字节搬运 - Java 关联:NIO 的
DirectByteBuffer利用 DMA 减少了数据在 JVM 堆内存和 OS 内存之间的拷贝次数。FileChannel.transferTo()在底层也会尝试使用 DMA / sendfile 零拷贝技术。
- 常见面试问题
- 冯·诺依曼架构的核心思想是什么?瓶颈是什么?
- 存储层次为什么分层?寄存器、Cache、内存、硬盘的速度量级是多少?
- 什么是缓存行?什么是伪共享?Java 中如何解决?
- MESI 协议的四种状态分别是什么?和 volatile 有什么关系?
- volatile 底层是如何通过内存屏障保证可见性和有序性的?
- 为什么排好序的数组遍历时 if 判断更快?(分支预测)
- CPU 流水线的三种冒险分别是什么?
- 内存屏障有哪几种?synchronized、volatile、final 分别对应什么屏障?
- 学习建议
- 不需要会设计 CPU,但需要理解"代码跑在硬件上的真实模型"。
- 重点掌握:存储层次 → CPU Cache → 缓存行/伪共享 → MESI → 内存屏障 → volatile/JMM。这是一条从硬件到 Java 的完整链路。
- 性能优化时先有证据(profiling),不要凭感觉猜。大多数业务代码的可读性比微小的缓存优化更重要。