CPU内存模型、Java内存模型和JVM内存模型

CPU内存模型、Java内存模型和JVM内存模型_第1张图片

目录

CPU内存模型

高速缓存

​编辑

缓存一致性问题

MESI协议

CPU乱序执行优化

CPU内存模型的几种类型

顺序一致性内存模型

不同类型的内存模型

Java内存模型(JMM)

Save和Load

happens-before

as-if-serial

Java内存模型和CPU内存模型的区别与联系

JVM内存模型

虚拟机栈

本地方法栈

PC寄存器

方法区

运行时常量池

JVM内存模型和Java内存模型对比

参考网址

相关拓展


CPU内存模型

中央处理器(central processing unit,简称CPU)作为计算机系统的运算和控制核心,是信息处理、程序运行的最终执行单元。CPU在处理任务时,常常需要和内存或硬盘进行交互,以此来完成特定的任务。随着科学技术的发展,根据摩尔定律,处理器的性能每隔两年翻一倍,而内存和磁盘的发展速度远不及CPU,这也导致了CPU的运算速度远高于访问内存和磁盘的I/O速度。

在计算机中,CPU 通过总线(bus)从主内存中读取数据,通常用内部时钟速度来描述 CPU 可以多快的执行操作,这个可以看作为 CPU 内执行的速度:即处理器的处理能力,但当 CPU 与计算机其他组件通信就比较慢了,这称之为外部时钟速度,当内部时钟速度大于外部时钟速度时,意味着 CPU 要等待,而从主内存中读取数据就是这样,当然,要使其它组件运行能跟得上 CPU 速度,代价非常昂贵。

高速缓存

为了弥补CPU和主内存处理能力的差距,大多数现代 CPU 采用在主内存和处理器之间引入了高速缓存(cache memory),并且高速缓存是分级的,一级缓存 L1 和二级缓存 L2 属于每个核,三级缓存 L3 为核共享的,当然,越靠近 CPU 其制作成本越高,因此 L3、L2、L1 的容量逐次递减,但访问速度却是递增的。

CPU内存模型、Java内存模型和JVM内存模型_第2张图片

单个cpu的缓存体系

CPU内存模型、Java内存模型和JVM内存模型_第3张图片

 多个cpu的缓存体系

CPU内存模型、Java内存模型和JVM内存模型_第4张图片

 本地电脑真实配置信息

有了高速缓存后,在执行内存读写操作时并不直接与主内存交互,而是通过高速缓存进行,当读写数据时,都会去缓存中找一下,若缓存中的数据可用,则直接操作缓存,若找不到或缓存中的数据不可用,会将所需要的数据从主内存中加载并放入响应的缓存中,也就是高速缓存中会保存主内存部分数据的副本,这显著的缓解了处理器瓶颈的问题。

CPU内存模型、Java内存模型和JVM内存模型_第5张图片

CPU访问各部件的速度

在写数据的时候一般采用 Write-back 的策略,是说写 cache 后,为了提高性能,并不立即写主内存而是等待一段时间将更新的值批量同步到主内存(还有一种方式是每次修改完都将数据同步到主内存,称之为 Write-Through,很少使用)。

缓存一致性问题

在引入了高速缓存后,在单个cpu的情况下,每个core都有自己的缓存,在多个cpu的情况下,每个cpu共享主内存,但是每个cpu和cpu中的core又有自己的高速缓存,这样就可能会出现各个core在处理同一个数据时,缓存不一致的情况,导致最后的运行结果不符合预期。

为了解决缓存一致性问题,一般有两种方式:

  • 通过在总线加LOCK#锁的方式
  • 通过缓存一致性协议

这2种方式都是硬件层面上提供的方式。

在早期的cpu当中,是通过在总线上加LOCK#锁的形式来解决缓存不一致的问题。core是cpu中的核心处理器,cpu中的处理逻辑基本都是由core来执行,而core和其他部件进行通信都是通过总线来进行的,如果对总线加LOCK#锁的话,也就是说阻塞了cpu中其他core对其他部件访问(如内存),从而使得只能有一个core能使用这个变量的内存。

当处理共享变量时,其中一个core获取到执行权限并对总线加LOCK#锁,其他的core就无法操作这个共享变量,当这个core执行完成后,其他core才能从共享变量所在的内存读取数据,这样就保证了缓存的一致性。但是这样的方式,由于在锁住总线期间,其他CPU无法访问内存,导致效率低下

MESI协议

由于总线加Lock锁的方式效率低下,后来便出现了缓存一致性协议。最出名的就是Intel 的MESI协议。

这种协议通过状态的流转来维护缓存间的一致性,并且它针对读取同一个地址的变量操作是并发,但更新一个地址的写操作是独占的,因此同一个变量的写操作在任意时刻只能由一个处理器执行,它把缓存中的缓存行(高速缓存与主内存交互的最小单元,类似于 MySQL,加载一条数据会把一块的数据都加载出来)分为 4 个状态(M、E、S、I)来保障数据一致性。

CPU内存模型、Java内存模型和JVM内存模型_第6张图片

MESI 通过定义一组消息用于协调各个处理器的读写内存操作,同时根据消息的内容会在上述 4 种状态间流转,CPU 在执行内存读写操作时通过总线发送特定的消息,同时其它处理器还会嗅探总线中由其它处理器发送的请求消息并会进行相应的回复,具体如下:

CPU内存模型、Java内存模型和JVM内存模型_第7张图片

MESI核心的思想是:当CPU写数据时,如果发现操作的变量是共享变量,即在其他CPU中也存在该变量的副本,会发出信号通知其他CPU将该变量的缓存行置为无效状态,因此当其他CPU需要读取这个变量时,发现自己缓存中缓存该变量的缓存行是无效的,那么它就会从内存重新读取。

CPU内存模型、Java内存模型和JVM内存模型_第8张图片

CPU乱序执行优化

除了增加高速缓存外,为了使得处理器内部的运算单元能够尽量被充分利用,处理器可能会对输入代码进行乱序执行(Out-Of-Order Execution)优化,处理器会在计算之后将乱序执行的结果重组,保证该结果与顺序执行是一致的,但不保证程序中各个语句执行的先后顺序和输入的顺序一致。Java虚拟机的即时编译器也有类似的指令重排序(Instrution Reorder)优化。

CPU内存模型的几种类型

顺序一致性内存模型

顺序一致性是一个被计算机科学家理想化了的理论参考模型,它为程序员提供了极强的内存可见性保证。顺序一致性内存模型有两大特性:

  • 一个线程中的所有操作必须按照程序的顺序执行。
  • (不管程序是否同步)所有线程都只能看到一个单一的操作执行顺序。在顺序一致性内存模型,每个操作都必须原子执行且立即对所有线程可见。

CPU内存模型、Java内存模型和JVM内存模型_第9张图片

在概念上,顺序一致性内存模型有一个单一的全局内存,这个内存通过一个左右摆动的开关可以连接到任意一个线程,同时每一个线程必须按照程序的顺序的顺序来执行内存读/写操作。

不同类型的内存模型

根据对顺序一致性内存模型不同类型的读/写操作组合的执行顺序执行放松,可以把常见处理器的内存模型划分为如下几种类型。

  • 放松程序中写-读的顺序,由此产生了Total Store Ordering内存模型(简称TSO)。
  • 在上面的基础上,继续放松程序中写-写操作的顺序,由此产生了Partial Store Ordering内存模型(简称为PSO)。
  • 在前面两条的基础上,继续放松程序中读-写和读-读操作顺序,由此产生了Relaxed Memory Order内存模型(简称为RMO)和PowerPC内存模型。

CPU内存模型、Java内存模型和JVM内存模型_第10张图片

各种处理器的内存模型,从上到下,模型由强变弱。越是追求性能的处理器,内存模型设计得就越弱。因为这些处理器希望内存模型对它们的束缚越少越好,这样它们就可以做尽可能多的优化来提高性能。 

Java内存模型(JMM)

不同架构的物理机器,可以拥有不一样的内存模型。而我们知道Java的宗旨就是:一次编译,到处运行。为了屏蔽掉各种硬件和操作系统的内存访问差异,以实现让Java程序在各个平台下都能达到一致的内存访问效果,Java虚拟机规范定义了Java内存模型(Java Memory Model,JMM)。

CPU内存模型、Java内存模型和JVM内存模型_第11张图片

Java内存模型规定了所有的变量(注意这里的变量包括了实例字段、静态字段和构成数组对象的元素,但不包括局部变量于方法参数,后者是线程私有的,不会被共享,自然就不会存在竞争问题。)都存储在主内存中。

每条线程还有自己的工作内存(Working Memory,可与前面讲得处理器高速缓存类比),线程的工作内存中保存了被该线程使用到的变量的主内存副本拷贝,线程对变量的所有操作(读取、赋值等)都必须在工作内存中进行,而不能直接读写主内存中的变量。

Save和Load

不同的线程之间无法直接访问对方工作内存中的变量,线程间变量值的传递均需要通过主内存完成。一个变量如何从主内存拷贝到工作内存,又如何从工作内存同步回主内存,JMM定义了一下8种操作来完成,虚拟机实现时必须保证每一个操作的原子性

  • lock(锁定) 作用于主内存的变量,将变量标识为一条线程独占的状态。
  • unlock(解锁) 作用于主内存的变量,将一个处于锁定状态的变量释放出来,释放后的变量可以被其他线程锁定。
  • read(读取) 作用于主内存的变量,将变量的值从主内存传输到线程的工作内存,以便后面的load操作。
  • load(载入) 作用于工作内存的变量,它把read操作从主内存中得到的变量值放入工作内存的变量副本中。
  • use(使用) 作用于工作内存的变量,它把工作内存中的一个变量的值传递给执行引擎,每当虚拟机遇到一个需要使用到变量的值的字节码指令时将会执行这个操作。
  • assign(赋值) 作用于工作内存的变量,它把一个从执行引擎收到的值赋给工作内存的变量,每当虚拟机遇到一个给变量赋值的字节码执行时执行这个操作。
  • store(存储) 作用于工作内存的变量,它把工作内存中一个变量的值传送到主内存中,以便随后的write操作使用。
  • write(写入) 作用于主内存的变量,它把store操作从工作内存中得到的变量的值放入主内存的变量中。

Java内存模型对上述8中基本操作做了规定,必须满足如下规则:

  • read和load、store和write必须顺序执行,注意是顺序而不是连续,也就是说read和load、store和write之间是可以插入其他指令的。
  • 不允许read和load、 store和write操作之一单独出现,即不允许一个变量从主内存读取了但工作内存不接受,或者从工作内存发起回写了但主内存不接受的情况出现。
  • 不允许一个线程丢弃它的最近的assign操作,即变量在工作内存中改变了之后必须把该变化同步回主内存。
  • 一个新的变量只能在主内存中“诞生”,不允许在工作内存中直接使用一个未被初始化(load或assign)的变量,换句话说,就是对一个变量实施use、store操作之前,必须先执行过了assign和load操作。
  • 一个变量在同一时刻只允许一条线程对其进行lock操作,但lock操作可以被同一条线程重复执行多次,多次执行lock后,只有执行相同次数的unlock操作,变量才会被解锁。
  • 如果对一个变量执行lock操作,那将会清空工作内存中的此变量的值,在执行引擎使用这个变量前,需要重新执行load或assign操作初始化变量的值
  • 如果一个变量事先没有被lock操作锁定,那就不允许对它执行unlock操作,也不允许去unlock一个被其他线程锁住的变量。
  • 对一个变量执行unlock操作之前,必须先把此变量同步回主内存中(执行store、write操作)。

这8中内存访问操作以及上述规则限定,再加上volatile关键字的一些特殊对象,就已经基本确定了Java程序中哪些内存访问操作在并发下是安全的。

这种定义相当严谨但又十分烦琐,实践起来很麻烦,并且早期的JMM存在一些漏洞。在JSR-133文档中,对这些漏洞进行了修复,并且放弃采用这8种操作去定义Java内存模型的访问协议了(仅是描述方式变了,Java内存模型并没有改变)。取而代之的是happens-before(先行发生)原则。

happens-before

JSR-133使用happens-before的概念来指定两个操作之间的执行顺序。由于这两个操作可以在一个线程之内,也可以是在不同线程之间。因此,JMM可以通过happens-before关系向程序员提供跨线程的内存可见性保证(如果A线程的写操作a与B线程的读操作b之间存在happens-before关系,尽管a操作和b操作在不同的线程中执行,但JMM向程序员保证a操作将对b操作可见)。

happens-before具有两层语义:

  1. 如果一个操作happens-before另一个操作,那么第一个操作的执行结果将对第二个操作可见,而且第一个操作的执行顺序排在第二个操作之前。
  2. 两个操作之间存在happens-before关系,并不意味着Java平台的具体实现必须要按照happens-before关系指定的顺序来执行。如果重排序之后的执行结果,与按happens-before关系来执行的结果一致,那么这种重排序并不非法(也就是说,JMM允许这种重排序)。

第一点是JMM对程序员的承诺。从程序员的角度来说,可以这样理解happens-before关系:如果A happens-before B,那么Java内存模型将向程序员保证——A操作的结果将对B可见,且A的执行顺序排在B之前。注意,这只是Java内存模型向程序员做出的保证!

第二点是JMM对编译器和处理器重排序的约束原则。正如前面所言,JMM其实是在遵循一个基本原则:只要不改变程序的执行结果(指的是单线程程序和正确同步的多线程程序),编译器和处理器怎么优化都行。JMM这么做的原因是:程序员对于这两个操作是否真的被重排序并不关心,程序员关心的是程序执行时的语义不能被改变(即执行结果不能被改变)。

happens-before具体的规则如下:

  1. 程序顺序规则(Program Order Rule):一个线程中的每个操作,happens-before于该线程中的任意后续操作。
  2. 监视器锁规则(Monitor Lock Rule):对一个锁的解锁,happens-before于随后对这个锁的加锁。
  3. volatile变量规则(Volatile Variable Rule):对一个volatile域的写,happens-before于任意后续对这个volatile域的读。
  4. 传递性(Transitivity):如果A happens-before B,且B happens-before C,那么A happens-before C。
  5. 线程启动规则(Thread Start Rule):如果线程A执行操作ThreadB.start()(启动线程B),那么A线程的ThreadB.start()操作happens-before于线程B中的任意操作。
  6. join()规则:如果线程A执行操作ThreadB.join()并成功返回,那么线程B中的任意操作happens-before于线程A从ThreadB.join()操作成功返回。
  7. 程序中断规则(Thread Interruption Rule):对线程interrupted()方法的调用先行于被中断线程的代码检测到中断时间的发生。
  8. 对象finalize规则(Finalizer Rule):一个对象的初始化完成(构造函数执行结束)先行于发生它的finalize()方法的开始。

在Java语言中这些先行发生规则无须任何同步手段就可以保障。

as-if-serial

前面已经提到过Java虚拟机的即时编译器也有类似的指令重排序(Instrution Reorder)优化。指令重排序一般分为以下三种:

指令并行的重排和内存系统的重排一般统称为处理器优化的重排。

as-if-serial的语义是处理器在进行重排序时必须要考虑指令之间的数据依赖性不管怎么重排序(编译器和处理器为了提高并行度),(单线程)程序的执行结果不能被改变。编译器和处理器都必须遵守as-if-serial语义。as-if-serial语义保证单线程内程序的执行结果不被改变,因此as-if-serial语义给编写单线程程序的程序员创造了一个幻境:单线程程序是按程序的顺序来执行的。

但是多线程环境中线程交替执行,两个线程中使用的变量能否保证一致性是无法确定的,结果无法预测。

Java内存模型和CPU内存模型的区别与联系

顺序一致性内存模型是一个理论参考模型,处理器内存模型是硬件级内存模型,而JMM是一个语言级的内存模型。语言内存模型、处理器内存模型和顺序一致性内存模型的强弱对比图如下:

CPU内存模型、Java内存模型和JVM内存模型_第12张图片

Java编译器在生成字节码时,会在执行指令序列的适当位置插入内存屏障来限制处理器的重排序。同时,由于各种处理器内存模型的强弱不同,为了在不同的处理器平台向程序员展示一个一致的内存模型,JMM在不同的处理器中需要插入的内存屏障的数量和种类也不同。

CPU内存模型、Java内存模型和JVM内存模型_第13张图片

总之,JMM屏蔽了不同处理器内存模型的差异,它在不同的处理器平台上为Java程序员呈现了一个一致的内存模型。

JVM内存模型

  Java虚拟机在执行Java程序的过程中会把它所管理的内存划分为若干和不同的数据区域。这些数据区域就是运行时数据区。每个区域都有各自的用途,以及创建和销毁时间。

CPU内存模型、Java内存模型和JVM内存模型_第14张图片

虚拟机栈

线程私有,生命周期和线程相同。每个方法在执行的同时都会创建一个栈帧(Stack Frame)用于存储局部变量表、操作数栈、动态链接、方法出口等信息。每个方法从调用到执行完成的过程,都对应着一个栈帧在虚拟机中入栈到出栈的过程。

局部变量表存放了编译期可知的各种基本数据类型、对象引用和returnAddress类型(指向了一条字节码指令的地址)。

Java虚拟机规范,规定了这个区域的两种异常状况:如果线程请求的栈深度大于虚拟机锁允许的深度,将抛出StackOverflowError;如果虚拟机可以动态扩展(大部分虚拟机都支持),在扩展时无法申请到足够的内存,将抛出OutOfMemoryError异常。

本地方法栈

线程私有,作用和虚拟机栈非常相似,只不过虚拟机栈为Java方法服务,本地方法栈为native方法服务。

PC寄存器

也叫程序计数器,程序计数器是一块较小的内存空间,它可以看做当前线程所执行的字节码的行号指示器,每个线程都有一个程序计数器。如果线程执行的是Java方法,程序计数器记录的是正在执行的虚拟机字节码指令的地址;如果正在执行的Native方法,这个计数器的值则为空。

Java虚拟机所管理的最大的一块内存,几乎所有的对象实例都在这里分配内存。被所有线程共享。 堆内存也是java gc发生的主要区域,因此也被称为GC堆。内内存继续细分的话,可以分为Eden区、From Survivor区、To Survivor区等。 堆内存可以是不连续的内存空间,只要逻辑上是连续的即可。可以通过-Xmx和-Xms控制大小。

方法区

线程共享,用于存储已被虚拟机加载的类信息、常量、静态变量、即时编辑器编译后的代码等数据。相对而言,GC很少发生在该区域。

运行时常量池

方法区有一个运行时常量池(Runtime Constant Pool),Class文件除了有类的版本、字段、方法接口等描述信息外,还有一项信息是常量池,用于存放编译期生成的各种字面量和符号引用,这部分内容将在类加载后进入方法区的运行时常量池中存放。

JVM内存模型和Java内存模型对比

JVM内存区域是根据运行时数据加载区域抽象出的模型,而JMM内存模型是根据运行时的工作机制抽象出的模型。

从定义来看,JVM内存模型中线程共享的堆和方法区对应JMM的主内存,线程私有的虚拟机栈、本地方法栈、程序计数器对应JMM的工作内存。

参考网址

CPU内存模型和Java内存模型以及Java内存区域 - 知乎

相关拓展

DCL问题

你可能感兴趣的:(java基础,java)