JVM进阶篇1 - 内存模型

JVM基础篇1 - Class的加载
JVM基础篇2 - 指令集
JVM进阶篇1 - 内存模型
JVM进阶篇2 - GC垃圾回收
JVM总览- JVM架构

先由硬件引入:

存储器的存储结构

在这里插入图片描述

1.两个cpu同时读取主存上的数据,怎样解决数据不一致性?

1.1 总线锁

在这里插入图片描述

老的cpu才会采用的方案:

问题: 其他CPU必须等前一个释放锁才能读取。

适用于:数据量大的情况

1.2 缓存锁

硬件层Cache一致性:MESI( Cache一致性协议)

在这里插入图片描述

缓存的四种状态:

  1. Modified 从主存里读过来是否修改过(修改过标记为Modified)
  2. Exclusive 从内存读过来 独享(标记为Exclusive)
  3. Shared 我在读的时候,别人也在读(标记为Shared)
  4. Invalid 我在读的时候,被别的CPU改过(标记为Invalid),观察到已经为invalid时就重新去主存里读
现在的cpu数据一致性实现 总线锁+缓存锁

1.3缓存行(Cache Line)

读取缓存时的最小单位: 64Byte ,无论读取的数据是否达到64Byte,都最少load 1 Cache Line 的数据进CPU。

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

每个缓存里面都是由缓存行组成的,缓存系统中以缓存行(cache line)为单位存储的。缓存行大小是64字节。由于缓存行的特性,当多线程修改互相独立的变量时,如果这些变量共享同一个缓存行,就会无意中影响彼此的性能,这就是伪共享(下面会介绍到)。有人将伪共享描述成无声的性能杀手,因为从代码中很难看清楚是否会出现伪共享问题。需要注意,数据在缓存中不是以独立的项来存储的,它不是我们认为的一个独立的变量,也不是一个单独的指针,它是有效引用主存中的一块地址。一个Java的long类型是8字节,因此在一个缓存行中可以存8个long类型的变量。

解决伪共享问题: 使用缓存行的对齐能提高效率,但会消耗少部分空间

扩展:

处理器为了提高处理速度,不直接和内存进行通讯,而是先将系统内存的数据读到内部缓存后再进行操作,但操作完之后不知道何时会写到内存;如果对声明了volatile 变量进行写操作,JVM就会向处理器发送一条Lock前缀的指令,将这个变量所在的缓存行的数据写回到系统内存。但就算写回到内存,如果其他处理器缓存的值还是旧的,再执行计算操作就会有问题,所以在多处理器下,为了保证各个处理器的缓存是一致的,就会实现缓存一致性协议,每个处理器通过嗅探在总线上传播的数据来检查自己缓存的值是不是过期了,当处理器发现自己缓存行对应的内存地址被修改,就会将当前处理器的缓存行设置成无效状态,当处理器要对这个数据进行修改操作的时候,会强制重新从系统内存里把数据读取到处理器缓存里

1.4 乱序问题:

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

合并写技术(write combining):

这里会用到合并写的 Buffer一共4个字节,也就是如果我们的修改在4个字节以内,则会使用到合并写的技术,如果超过4个字节,则无法使用到合写的红利。 CPU中这4个字节的数据非常快速flush

如何保证特定情况下不乱序? Valatile关键字

1.4.1 CPU级别的内存屏障:

在这里插入图片描述

屏障两边的指令不能混到一起

  • sfence : (save fence)前面的写的操作必须在后面写的操作之前执行
  • lfence: (load fence)前面的读的操作必须在后面读的操作之前执行
  • mfence: (全屏障)前面的读写的操作必须在后面读写的操作之前执行。
1.4.2 原子指令 Lock指令

在所有的X86的CPU上都具有锁定一个特定内存地址的能力,当这个特定内存地址被锁定之后,它就可以阻止其他的系统总线读取或修改这个内存地址。这种能力是通过Lock指令前缀加上下面的汇编指令来实现的。当使用Lock前缀时,它会使CPU宣告一个Lock#信号,这样就能确保在多处理器系统或多线程竞争的环境下互斥地使用这个内存地址。当指令执行完毕,这个锁定动作也就会消失。

1.4.3 JVM层级的规范:

在这里插入图片描述

2. volatile实现细节

1. 字节码层面 Access flags 标识为volatile

在这里插入图片描述

2. JVM层面 内存区的读写都加屏障

在这里插入图片描述

3. OS和硬件层面

  • windows 使用lock指令实现

3.synchronized 实现细节

1.字节码层面monitorenter monitorexit):

  • 1.1 synchronized 关键字

在这里插入图片描述

  • 1.2 synchronized{ … } 代码块
    在这里插入图片描述

中间的monitorexit是在代码块出现异常时释放锁。

2.JVM层面
  • C C++调用了操作系统提供的同步机制
3.OS和硬件层面
  • Lock指令compare and exchange

hanppens-before 原则:

在这里插入图片描述

4.对象的内存布局:

4.1 对象创建的过程?

在这里插入图片描述

4.2 对象在内存中的存储布局?

普通对象:

  1. 对象头: markword 8字节
  2. ClassPointer指针(属于哪个Class)
  3. 实例数据(成员属性)
  4. padding 对齐 8的倍数

数组对象:

  1. 对象头 markword 8字节
  2. ClassPointer指针(属于哪个Class)
  3. 数组长度: 4字节
  4. 数组数据
  5. padding对齐 8的倍数

4.3 对象头具体包括什么?

重点: 是否为偏向锁,锁标志位 ,GC标记

在这里插入图片描述

分代年龄最大15岁(分代年龄占4个bit 最大1111)

4.4 对象怎么定位?

  • 直接指针:直接寻址,访问速度快
  • 句柄方式:间接寻址,垃圾回收起来比较稳定

5.JMM Java Memory Model(java内存模型)

概念:Java内存模型(Java Memory Model,JMM)JMM主要是为了规定了线程和内存之间的一些关系。根据JMM的设计,系统存在一个主内存(Main Memory),Java中所有变量都储存在主存中,对于所有线程都是共享的。每条线程都有自己的工作内存(Working Memory),工作内存中保存的是主存中某些变量的拷贝,线程对所有变量的操作都是在工作内存中进行,线程之间无法相互直接访问,变量传递均需要通过主存完成。

作用

缓存一致性协议,用来定义读写的规则

JMM定义了线程工作内存和主内存之间的抽象关系:线程之间的共享变量存储在主内存(Main Memory)中,每个线程都有一个私有的本地内存(Local Memory)

解决共享对象可见性这个问题:java volatile 关键字

在这里插入图片描述

关键词synchronized与volatile总结
synchronized的特点
一个线程执行互斥代码过程如下:

  1. 获得同步锁
  2. 清空工作内存
  3. 从主内存拷贝对象副本到工作内存
  4. 执行代码(计算或者输出等)
  5. 刷新主内存数据
  6. 释放同步锁

所以,synchronized既保证了多线程的并发有序性,又保证了多线程的内存可见性。

volatile是第二种Java多线程同步的手段,根据JLS的说法,一个变量可以被volatile修饰,在这种情况下内存模型确保所有线程可以看到一致的变量值

class Test {
    
      
    static volatile int i = 0, j = 0;  
    static void one() {
    
      
        i++;  
        j++;  
    }  
    static void two() {
    
      
        System.out.println("i=" + i + " j=" + j);  
    }  
}  

加上volatile可以将共享变量i和j的改变直接响应到主内存中,这样保证了i和j的值可以保持一致,然而我们不能保证执行two方法的线程是在i和j执行到什么程度获取到的,所以volatile可以保证内存可见性,不能保证并发有序性
如果没有volatile,则代码执行过程如下:

  1. 将变量i从主内存拷贝到工作内存;
  2. 刷新主内存数据;
  3. 改变i的值;
  4. 将变量j从主内存拷贝到工作内存;
  5. 刷新主内存数据;
  6. 改变j的值;

猜你喜欢

转载自blog.csdn.net/The_xiaoke/article/details/124261799
今日推荐