目录
CPU内存模型
高速缓存
编辑
缓存一致性问题
MESI协议
CPU乱序执行优化
CPU内存模型的几种类型
顺序一致性内存模型
不同类型的内存模型
Java内存模型(JMM)
Save和Load
happens-before
as-if-serial
Java内存模型和CPU内存模型的区别与联系
JVM内存模型
虚拟机栈
本地方法栈
PC寄存器
堆
方法区
运行时常量池
JVM内存模型和Java内存模型对比
参考网址
相关拓展
中央处理器(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的缓存体系
多个cpu的缓存体系
本地电脑真实配置信息
有了高速缓存后,在执行内存读写操作时并不直接与主内存交互,而是通过高速缓存进行,当读写数据时,都会去缓存中找一下,若缓存中的数据可用,则直接操作缓存,若找不到或缓存中的数据不可用,会将所需要的数据从主内存中加载并放入响应的缓存中,也就是高速缓存中会保存主内存部分数据的副本,这显著的缓解了处理器瓶颈的问题。
CPU访问各部件的速度
在写数据的时候一般采用 Write-back 的策略,是说写 cache 后,为了提高性能,并不立即写主内存而是等待一段时间将更新的值批量同步到主内存(还有一种方式是每次修改完都将数据同步到主内存,称之为 Write-Through,很少使用)。
在引入了高速缓存后,在单个cpu的情况下,每个core都有自己的缓存,在多个cpu的情况下,每个cpu共享主内存,但是每个cpu和cpu中的core又有自己的高速缓存,这样就可能会出现各个core在处理同一个数据时,缓存不一致的情况,导致最后的运行结果不符合预期。
为了解决缓存一致性问题,一般有两种方式:
这2种方式都是硬件层面上提供的方式。
在早期的cpu当中,是通过在总线上加LOCK#锁的形式来解决缓存不一致的问题。core是cpu中的核心处理器,cpu中的处理逻辑基本都是由core来执行,而core和其他部件进行通信都是通过总线来进行的,如果对总线加LOCK#锁的话,也就是说阻塞了cpu中其他core对其他部件访问(如内存),从而使得只能有一个core能使用这个变量的内存。
当处理共享变量时,其中一个core获取到执行权限并对总线加LOCK#锁,其他的core就无法操作这个共享变量,当这个core执行完成后,其他core才能从共享变量所在的内存读取数据,这样就保证了缓存的一致性。但是这样的方式,由于在锁住总线期间,其他CPU无法访问内存,导致效率低下
由于总线加Lock锁的方式效率低下,后来便出现了缓存一致性协议。最出名的就是Intel 的MESI协议。
这种协议通过状态的流转来维护缓存间的一致性,并且它针对读取同一个地址的变量操作是并发,但更新一个地址的写操作是独占的,因此同一个变量的写操作在任意时刻只能由一个处理器执行,它把缓存中的缓存行(高速缓存与主内存交互的最小单元,类似于 MySQL,加载一条数据会把一块的数据都加载出来)分为 4 个状态(M、E、S、I)来保障数据一致性。
MESI 通过定义一组消息用于协调各个处理器的读写内存操作,同时根据消息的内容会在上述 4 种状态间流转,CPU 在执行内存读写操作时通过总线发送特定的消息,同时其它处理器还会嗅探总线中由其它处理器发送的请求消息并会进行相应的回复,具体如下:
MESI核心的思想是:当CPU写数据时,如果发现操作的变量是共享变量,即在其他CPU中也存在该变量的副本,会发出信号通知其他CPU将该变量的缓存行置为无效状态,因此当其他CPU需要读取这个变量时,发现自己缓存中缓存该变量的缓存行是无效的,那么它就会从内存重新读取。
除了增加高速缓存外,为了使得处理器内部的运算单元能够尽量被充分利用,处理器可能会对输入代码进行乱序执行(Out-Of-Order Execution)优化,处理器会在计算之后将乱序执行的结果重组,保证该结果与顺序执行是一致的,但不保证程序中各个语句执行的先后顺序和输入的顺序一致。Java虚拟机的即时编译器也有类似的指令重排序(Instrution Reorder)优化。
顺序一致性是一个被计算机科学家理想化了的理论参考模型,它为程序员提供了极强的内存可见性保证。顺序一致性内存模型有两大特性:
在概念上,顺序一致性内存模型有一个单一的全局内存,这个内存通过一个左右摆动的开关可以连接到任意一个线程,同时每一个线程必须按照程序的顺序的顺序来执行内存读/写操作。
根据对顺序一致性内存模型不同类型的读/写操作组合的执行顺序执行放松,可以把常见处理器的内存模型划分为如下几种类型。
各种处理器的内存模型,从上到下,模型由强变弱。越是追求性能的处理器,内存模型设计得就越弱。因为这些处理器希望内存模型对它们的束缚越少越好,这样它们就可以做尽可能多的优化来提高性能。
不同架构的物理机器,可以拥有不一样的内存模型。而我们知道Java的宗旨就是:一次编译,到处运行。为了屏蔽掉各种硬件和操作系统的内存访问差异,以实现让Java程序在各个平台下都能达到一致的内存访问效果,Java虚拟机规范定义了Java内存模型(Java Memory Model,JMM)。
Java内存模型规定了所有的变量(注意这里的变量包括了实例字段、静态字段和构成数组对象的元素,但不包括局部变量于方法参数,后者是线程私有的,不会被共享,自然就不会存在竞争问题。)都存储在主内存中。
每条线程还有自己的工作内存(Working Memory,可与前面讲得处理器高速缓存类比),线程的工作内存中保存了被该线程使用到的变量的主内存副本拷贝,线程对变量的所有操作(读取、赋值等)都必须在工作内存中进行,而不能直接读写主内存中的变量。
不同的线程之间无法直接访问对方工作内存中的变量,线程间变量值的传递均需要通过主内存完成。一个变量如何从主内存拷贝到工作内存,又如何从工作内存同步回主内存,JMM定义了一下8种操作来完成,虚拟机实现时必须保证每一个操作的原子性。
Java内存模型对上述8中基本操作做了规定,必须满足如下规则:
这8中内存访问操作以及上述规则限定,再加上volatile关键字的一些特殊对象,就已经基本确定了Java程序中哪些内存访问操作在并发下是安全的。
这种定义相当严谨但又十分烦琐,实践起来很麻烦,并且早期的JMM存在一些漏洞。在JSR-133文档中,对这些漏洞进行了修复,并且放弃采用这8种操作去定义Java内存模型的访问协议了(仅是描述方式变了,Java内存模型并没有改变)。取而代之的是happens-before(先行发生)原则。
JSR-133使用happens-before的概念来指定两个操作之间的执行顺序。由于这两个操作可以在一个线程之内,也可以是在不同线程之间。因此,JMM可以通过happens-before关系向程序员提供跨线程的内存可见性保证(如果A线程的写操作a与B线程的读操作b之间存在happens-before关系,尽管a操作和b操作在不同的线程中执行,但JMM向程序员保证a操作将对b操作可见)。
happens-before具有两层语义:
第一点是JMM对程序员的承诺。从程序员的角度来说,可以这样理解happens-before关系:如果A happens-before B,那么Java内存模型将向程序员保证——A操作的结果将对B可见,且A的执行顺序排在B之前。注意,这只是Java内存模型向程序员做出的保证!
第二点是JMM对编译器和处理器重排序的约束原则。正如前面所言,JMM其实是在遵循一个基本原则:只要不改变程序的执行结果(指的是单线程程序和正确同步的多线程程序),编译器和处理器怎么优化都行。JMM这么做的原因是:程序员对于这两个操作是否真的被重排序并不关心,程序员关心的是程序执行时的语义不能被改变(即执行结果不能被改变)。
happens-before具体的规则如下:
在Java语言中这些先行发生规则无须任何同步手段就可以保障。
前面已经提到过Java虚拟机的即时编译器也有类似的指令重排序(Instrution Reorder)优化。指令重排序一般分为以下三种:
指令并行的重排和内存系统的重排一般统称为处理器优化的重排。
as-if-serial的语义是处理器在进行重排序时必须要考虑指令之间的数据依赖性,不管怎么重排序(编译器和处理器为了提高并行度),(单线程)程序的执行结果不能被改变。编译器和处理器都必须遵守as-if-serial语义。as-if-serial语义保证单线程内程序的执行结果不被改变,因此as-if-serial语义给编写单线程程序的程序员创造了一个幻境:单线程程序是按程序的顺序来执行的。
但是多线程环境中线程交替执行,两个线程中使用的变量能否保证一致性是无法确定的,结果无法预测。
顺序一致性内存模型是一个理论参考模型,处理器内存模型是硬件级内存模型,而JMM是一个语言级的内存模型。语言内存模型、处理器内存模型和顺序一致性内存模型的强弱对比图如下:
Java编译器在生成字节码时,会在执行指令序列的适当位置插入内存屏障来限制处理器的重排序。同时,由于各种处理器内存模型的强弱不同,为了在不同的处理器平台向程序员展示一个一致的内存模型,JMM在不同的处理器中需要插入的内存屏障的数量和种类也不同。
总之,JMM屏蔽了不同处理器内存模型的差异,它在不同的处理器平台上为Java程序员呈现了一个一致的内存模型。
Java虚拟机在执行Java程序的过程中会把它所管理的内存划分为若干和不同的数据区域。这些数据区域就是运行时数据区。每个区域都有各自的用途,以及创建和销毁时间。
线程私有,生命周期和线程相同。每个方法在执行的同时都会创建一个栈帧(Stack Frame)用于存储局部变量表、操作数栈、动态链接、方法出口等信息。每个方法从调用到执行完成的过程,都对应着一个栈帧在虚拟机中入栈到出栈的过程。
局部变量表存放了编译期可知的各种基本数据类型、对象引用和returnAddress类型(指向了一条字节码指令的地址)。
Java虚拟机规范,规定了这个区域的两种异常状况:如果线程请求的栈深度大于虚拟机锁允许的深度,将抛出StackOverflowError;如果虚拟机可以动态扩展(大部分虚拟机都支持),在扩展时无法申请到足够的内存,将抛出OutOfMemoryError异常。
线程私有,作用和虚拟机栈非常相似,只不过虚拟机栈为Java方法服务,本地方法栈为native方法服务。
也叫程序计数器,程序计数器是一块较小的内存空间,它可以看做当前线程所执行的字节码的行号指示器,每个线程都有一个程序计数器。如果线程执行的是Java方法,程序计数器记录的是正在执行的虚拟机字节码指令的地址;如果正在执行的Native方法,这个计数器的值则为空。
Java虚拟机所管理的最大的一块内存,几乎所有的对象实例都在这里分配内存。被所有线程共享。 堆内存也是java gc发生的主要区域,因此也被称为GC堆。内内存继续细分的话,可以分为Eden区、From Survivor区、To Survivor区等。 堆内存可以是不连续的内存空间,只要逻辑上是连续的即可。可以通过-Xmx和-Xms控制大小。
线程共享,用于存储已被虚拟机加载的类信息、常量、静态变量、即时编辑器编译后的代码等数据。相对而言,GC很少发生在该区域。
方法区有一个运行时常量池(Runtime Constant Pool),Class文件除了有类的版本、字段、方法接口等描述信息外,还有一项信息是常量池,用于存放编译期生成的各种字面量和符号引用,这部分内容将在类加载后进入方法区的运行时常量池中存放。
JVM内存区域是根据运行时数据加载区域抽象出的模型,而JMM内存模型是根据运行时的工作机制抽象出的模型。
从定义来看,JVM内存模型中线程共享的堆和方法区对应JMM的主内存,线程私有的虚拟机栈、本地方法栈、程序计数器对应JMM的工作内存。
CPU内存模型和Java内存模型以及Java内存区域 - 知乎
DCL问题