JVM - JMM内存模型

文章目录
  1. 1. 一、CPU和内存的交互
  2. 2. 二、缓存行
    1. 2.1. 伪共享问题 (缓存行对齐提高效率)
    2. 2.2. 硬件层数据一致性
      1. 2.2.1. MESI协议中的状态
  3. 3. 三、乱序问题
    1. 3.1. 内存屏障(Memory Barrier) 保证特定情况下不乱序
      1. 3.1.1. 硬件内存屏障 X86
      2. 3.1.2. JVM级别如何规范(JSR133)
      3. 3.1.3. volatile的实现细节
      4. 3.1.4. synchronized实现细节

一、CPU和内存的交互

在计算机中,CPU和内存的交互最为频繁,相比内存,磁盘读写太慢,内存相当于高速的缓冲区。

在多核CPU中,每个处理器都有各自的高速缓存(L1,L2,L3),而主内存确只有一个 。

越靠近CPU的缓存越快也越小。所以L1缓存(一级缓存)很小但很快,并且紧靠着在使用它的CPU内核。L2大一些,也慢一些,并且仍然只能被一个单独的 CPU 核使用。L3在现代多核机器中更普遍,仍然更大,更慢,并且被单个插槽上的所有 CPU 核共享。

当CPU执行运算的时候,它先去L1查找所需的数据,再去L2,然后是L3,最后如果这些缓存中都没有,所需的数据就要去主内存拿。走得越远,运算耗费的时间就越长。

从CPU到 大约需要的CPU 周期 大约需要的时间
主存 约60-80纳秒
QPI总线传输(between sockets, not drawn) 约20ns
L3 cache 约40-45 cycles 约15ns
L2 cache 约10 cycles 约3ns
L1 cache 约3-4 cycles 约1ns
寄存器 1 cycle

二、缓存行

数据在缓存中不是以独立的项来存储的,缓存是由缓存行组成的,通常是64字节,并且它有效地引用主内存中的一块地址。一个Java的long类型是8字节,因此在一个缓存行中可以存8个long类型的变量。
非常奇妙的是如果你访问一个long数组,当数组中的一个值被加载到缓存中,它会额外加载另外7个。因此你能非常快地遍历这个数组。
因此如果数据结构中的项在内存中不是彼此相邻的(链表),你将得不到免费缓存加载所带来的优势。并且在这些数据结构中的每一个项都可能会出现缓存未命中。

伪共享问题 (缓存行对齐提高效率)

但这种加载有一个弊端。设想long类型的数据不是数组的一部分,它只是一个单独的变量。让我们称它为head。然后再设想有另一个变量tail紧挨着它。当加载head到缓存的时候同时也加载了tail。
tail正在被你的生产者写入,而head正在被你的消费者写入。这两个变量实际上并不是密切相关的,而事实上却要被两个不同内核中运行的线程所使用。

设想消费者更新了head的值。缓存中的值和内存中的值都被更新了,而其他所有存储head的缓存行都会都会失效。请记住我们必须以整个缓存行作为单位来处理,不能只把head标记为无效。
现在如果一些正在其他内核中运行的进程只是想读tail的值,整个缓存行需要从主内存重新读取。那么一个和你的消费者无关的线程读一个和head无关的值,它被缓存未命中给拖慢了。

硬件层数据一致性

MESI(Modified Exclusive Shared Or Invalid)–CPU缓存一致性协议, 是一种广泛使用的支持写回策略的缓存一致性协议。

MESI协议中的状态

CPU中每个缓存行(caceh line)使用4种状态进行标记,CPU在每个cache line 额外两位标记四种状态(使用额外的两位(bit)表示):

M: 被修改(Modified)
该缓存行只被缓存在该CPU的缓存中,并且是被修改过的(dirty),即与主存中的数据不一致,该缓存行中的内存需要在未来的某个时间点(允许其它CPU读取请主存中相应内存之前)写回(write back)主存。 当被写回主存之后,该缓存行的状态会变成独享(exclusive)状态。

E: 独享的(Exclusive)
该缓存行只被缓存在该CPU的缓存中,它是未被修改过的(clean),与主存中数据一致。该状态可以在任何时刻当有其它CPU读取该内存时变成共享状态(shared)。同样地,当CPU修改该缓存行中内容时,该状态可以变成Modified状态。

S:共享的(Shared)
该状态意味着该缓存行可能被多个CPU缓存,并且各个缓存中的数据与主存数据一致(clean),当有一个CPU修改该缓存行中,其它CPU中该缓存行可以被作废(变成无效状态(Invalid))。

I: 无效的(Invalid)
该缓存是无效的(可能有其它CPU修改了该缓存行)。

三、乱序问题

CPU为了提高指令执行效率,会在一条指令执行过程中(比如去内存读数据(慢100倍)),去同时执行另一条指令,前提是,两条指令没有依赖关系。

内存屏障(Memory Barrier) 保证特定情况下不乱序

CPU执行指令可能是无序的,内存屏障有两个比较重要的作用

  1. 阻止屏障两侧指令重排序
  2. 强制把写缓冲区/高速缓存中的脏数据等写回主内存,让缓存中相应的数据失效。
硬件内存屏障 X86
  • sfence: store| 在sfence指令前的写操作当必须在sfence指令后的写操作前完成。
  • lfence:load | 在lfence指令前的读操作当必须在lfence指令后的读操作前完成。
  • mfence:modify/mix | 在mfence指令前的读写操作当必须在mfence指令后的读写操作前完成。
  • 原子指令,如x86上的”lock …” 指令是一个Full Barrier,执行时会锁住内存子系统来确保执行顺序,甚至跨多个CPU。Software Locks通常使用了内存屏障或原子指令来实现变量可见性和保持程序顺序
JVM级别如何规范(JSR133)
  • LoadLoad屏障:
    在Load2及后续读取操作要读取的数据被访问前,保证Load1要读取的数据被读取完毕。

  • StoreStore屏障:
    在Store2及后续写入操作执行前,保证Store1的写入操作对其它处理器可见。

  • LoadStore屏障:
    在Store2及后续写入操作被刷出前,保证Load1要读取的数据被读取完毕。

  • StoreLoad屏障:
    在Load2及后续所有读取操作执行前,保证Store1的写入对所有处理器可见。

volatile的实现细节
  1. 字节码层面

    • ACC_VOLATILE
  2. JVM层面, Volatile内存区的读写都加屏障

    • StoreStoreBarrier
    • volatile 写操作
    • StoreLoadBarrier
    • LoadLoadBarrier
    • volatile 读操作
    • LoadStoreBarrier
synchronized实现细节
  1. 字节码层面

    • ACC_SYNCHRONIZED
    • monitorenter monitorexit
  2. JVM层面

    • C C++ 调用了操作系统提供的同步机制