JVM 从入门到入土 ②:内存加载过程

缓存行

内存数据放入高速缓存中时是以缓存行的形式放入的,多数情况把内存中连续的 64 字节的数据作为一个缓存行加入高速缓存,不会只单独放入几个字节的数据

伪共享问题: 位于同一缓存行的两个不同数据,被两个不同 CPU 锁定,产生互相影响的伪共享问题

缓存行对齐

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
public class CacheLinePadding {
// 加上 padding,使 arr[0] 和 arr[1] 在两个不同的缓存行中
// 使运行速度提升
private static class Padding {
public volatile long p1, p2, p3, p4, p5, p6, p7;
}

private static class T extends Padding {
public volatile long x = 0L;
}

public static T[] arr = new T[2];

static {
arr[0] = new T();
arr[1] = new T();
}

public static void main(String[] args) throws Exception {
Thread t1 = new Thread(() -> {
for (long i = 0; i < 10000_0000L; i++) {
arr[0].x = i;
}
});

Thread t2 = new Thread(() -> {
for (long i = 0; i < 10000_0000L; i++) {
arr[1].x = i;
}
});

final long start = System.nanoTime();
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println((System.nanoTime() - start) / 100_0000 + " ms");
}
}

缓存锁: MESI
【并发编程】MESI–CPU 缓存一致性协议

总线锁: 有些无法被缓存的数据或者跨越多个缓存行的数据还是得使用总线锁

指令重排

验证 JVM/jmm/Disorder.java
现代 cpu 的合并写技术对程序的影响

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

  • 读指令的同时可以执行不影响的其他指令
  • 写指令的同时可以进行合并写
    WCBuffers(Write Combining Buffers),只能存放 4 字节的数据。用于写指令合并缓存。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    public final class WriteCombining {

    private static final int ITERATIONS = Integer.MAX_VALUE;
    private static final int ITEMS = 1 << 24;
    private static final int MASK = ITEMS - 1;

    private static final byte[] arrayA = new byte[ITEMS];
    private static final byte[] arrayB = new byte[ITEMS];
    private static final byte[] arrayC = new byte[ITEMS];
    private static final byte[] arrayD = new byte[ITEMS];
    private static final byte[] arrayE = new byte[ITEMS];
    private static final byte[] arrayF = new byte[ITEMS];

    public static void main(final String[] args) {

    for (int i = 1; i <= 3; i++) {
    System.out.println(i + " SingleLoop duration (ns) = " + runCaseOne());
    System.out.println(i + " SplitLoop duration (ns) = " + runCaseTwo());
    }
    }

    public static long runCaseOne() {
    long start = System.nanoTime();
    int i = ITERATIONS;

    while (--i != 0) {
    int slot = i & MASK;
    byte b = (byte) i;
    arrayA[slot] = b;
    arrayB[slot] = b;
    arrayC[slot] = b;
    arrayD[slot] = b;
    arrayE[slot] = b;
    arrayF[slot] = b;
    }
    return System.nanoTime() - start;
    }

    public static long runCaseTwo() {
    long start = System.nanoTime();
    int i = ITERATIONS;
    while (--i != 0) {
    int slot = i & MASK;
    byte b = (byte) i;
    arrayA[slot] = b;
    arrayB[slot] = b;
    arrayC[slot] = b;
    }
    i = ITERATIONS;
    while (--i != 0) {
    int slot = i & MASK;
    byte b = (byte) i;
    arrayD[slot] = b;
    arrayE[slot] = b;
    arrayF[slot] = b;
    }
    return System.nanoTime() - start;
    }
    }

如何保证不重排:

  • CPU 内存屏障(硬件方面实现):Inter X86
    • sfence:在 sfence 指令前的写操作必须在 sfence 指令后的写操作前完成
    • lfence:在 lfence 指令前的读操作必须在 lfence 指令后的读操作前完成
    • mfence:在 mfence 指令前的写操作必须在 mfence 指令后的读操作前完成
  • CPU lock 原子汇编指令(硬件方面实现):Full Barrier,执行时会锁住内存子系统来确保执行顺序,甚至跨多个 CPU
  • JVM 规范(JSR 133):依赖于硬件实现
    • LoadLoad 屏障:
      保证读操作 Load1 先于读操作 Load2 执行

      1
      2
      3
      Load1 操作
      LoadLoad 屏障
      Load2 操作
    • StoreStore 屏障:
      保证写操作 Store1 先于写操作 Store2 执行

1
2
3
Store1 操作
StoreStore 屏障
Store2 操作
  • LoadStore 屏障:
    保证读操作 Load1 先于写操作 Store2 执行

    1
    2
    3
    Load1 操作
    LoadStore 屏障
    Store2 操作
  • StoreLoad 屏障:
    保证写操作 Store1 先于读操作 Load2 执行

    1
    2
    3
    Store1 操作
    StoreLoad 屏障
    Load2 操作

volatile 实现细节

  1. 字节码层面:ACC_VOLATILE 编码

  2. JVM 层面:

  3. 写操作

    1
    2
    3
    StoreStore 屏障
    volatile 写操作
    StoreLoad 屏障
  4. 读操作

    1
    2
    3
    LoadLoad 屏障
    volatile 读操作
    LoadStore 屏障
  5. OS 和硬件层面:
    volatile 与 lock 前缀指令

synchronized 实现细节

  1. 字节码层面
    ACC_SYNCHRONIZED

    1
    2
    3
    4
    5
    6
    7
    8
    ; 加锁
    monitorenter
    ...
    ; 释放锁
    monitorexit
    ...
    ; 异常后释放锁
    monitorexit
  2. JVM 层面
    C C++ 调用了操作系统提供的同步机制

  3. OS 和硬件层面
    X86 : lock cmpxchg / xxx
    Java 使用字节码和汇编语言同步分析 volatile,synchronized 的底层实现

happens-before 原则