前提知识:
Java内存模型(JMM)是一个概念模型,底层是计算机的寄存器、缓存内存、主内存和CPU等。
多处理器环境下,共享数据的交互硬件设备之间的关系:
JMM:
从以上两张图中,谈一谈以下几个概念:
1.缓存一致性协议(MESI):
由于每个处理器都含有私有的高速缓存,在对缓存中数据进行更新后,其他处理器中所含有的该共享变量的缓存如果被处理器进行读操作,就会出现错误。有些计算机采用LOCK#信号对总线进行锁定,当一个处理器在总线上输出此信号时,其它处理器的请求将被阻塞,那么该处理器就能独自共享内存。然而总线锁定的开销太大,在之后的计算机中一般都采用“缓存锁定”的方式实现。
MESI是代表了缓存数据的四种状态的首字母,分别是Modified、Exclusive、Shared、Invalid)
M(Modified):被修改的。处于这一状态的数据,只在本CPU中有缓存数据,而其他CPU中没有。同时其状态相对于内存中的值来说,是已经被修改的,且没有更新到内存中。
E(Exclusive):独占的。处于这一状态的数据,只有在本CPU中有缓存,且其数据没有修改,即与内存中一致。
S(Shared):共享的。处于这一状态的数据在多个CPU中都有缓存,且与内存一致。
I(Invalid):要么已经不在缓存中,要么它的内容已经过时。为了达到缓存的目的,这种状态的段将会被忽略。一旦缓存段被标记为失效,那效果就等同于它从来没被加载到缓存中。
在缓存行中有这四种状态的基础上,通过“嗅探”技术完成以下功能:【嗅探技术能够嗅探其他处理器访问主内存和它们的内部缓存】
- 一个处于M状态的缓存行,必须时刻监听所有试图读取该缓存行对应的主存地址的操作,如果监听到,则必须在此操作执行前把其缓存行中的数据写回CPU。
- 一个处于S状态的缓存行,必须时刻监听使该缓存行无效或者独享该缓存行的请求,如果监听到,则必须把其缓存行状态设置为I。
- 一个处于E状态的缓存行,必须时刻监听其他试图读取该缓存行对应的主存地址的操作,如果监听到,则必须把其缓存行状态设置为S。
- 只有E和M可以进行写操作而且不需要额外操作,如果想对S状态的缓存字段进行写操作,那必须先发送一个RFO(Request-For-Ownership)广播,该广播可以让其他CPU的缓存中的相同数据的字段实效,即变成I状态。
通过以上机制可以使得处理器在每次读写操作都是原子的,并且每次读到的数据都是最新的。
为什么要有内存模型
在介绍Java内存模型之前,先来看一下到底什么是计算机内存模型,然后再来看Java内存模型在计算机内存模型的基础上做了哪些事情。要说计算机的内存模型,就要说一下一段古老的历史,看一下为什么要有内存模型。
内存模型,英文名Memory Model,他是一个很老的老古董了。他是与计算机硬件有关的一个概念。那么我先给你介绍下他和硬件到底有啥关系。
CPU和缓存一致性
我们应该都知道,计算机在执行程序的时候,每条指令都是在CPU中执行的,而执行的时候,又免不了要和数据打交道。而计算机上面的数据,是存放在主存当中的,也就是计算机的物理内存啦。
刚开始,还相安无事的,但是随着CPU技术的发展,CPU的执行速度越来越快。而由于内存的技术并没有太大的变化,所以从内存中读取和写入数据的过程和CPU的执行速度比起来差距就会越来越大,这就导致CPU每次操作内存都要耗费很多等待时间。
可是,不能因为内存的读写速度慢,就不发展CPU技术了吧,总不能让内存成为计算机处理的瓶颈吧。
所以,人们想出来了一个好的办法,就是在CPU和内存之间增加高速缓存。缓存的概念大家都知道,就是保存一份数据拷贝。他的特点是速度快,内存小,并且昂贵。
那么,程序的执行过程就变成了:当程序在运行过程中,会将运算需要的数据从主存复制一份到CPU的高速缓存当中,那么CPU进行计算时就可以直接从它的高速缓存读取数据和向其中写入数据,当运算结束之后,再将高速缓存中的数据刷新到主存当中。
而随着CPU能力的不断提升,一层缓存就慢慢的无法满足要求了,就逐渐的衍生出多级缓存。
按照数据读取顺序和与CPU结合的紧密程度,CPU缓存可以分为一级缓存(L1),二级缓存(L3),部分高端CPU还具有三级缓存(L3),每一级缓存中所储存的全部数据都是下一级缓存的一部分。
这三种缓存的技术难度和制造成本是相对递减的,所以其容量也是相对递增的。
那么,在有了多级缓存之后,程序的执行就变成了:当CPU要读取一个数据时,首先从一级缓存中查找,如果没有找到再从二级缓存中查找,如果还是没有就从三级缓存或内存中查找。
单核CPU只含有一套L1,L2,L3缓存;如果CPU含有多个核心,即多核CPU,则每个核心都含有一套L1(甚至和L2)缓存,而共享L3(或者和L2)缓存。
下图为一个单CPU双核的缓存结构:
随着计算机能力不断提升,开始支持多线程。那么问题就来了。我们分别来分析下单线程、多线程在单核CPU、多核CPU中的影响。
单线程。cpu核心的缓存只被一个线程访问。缓存独占,不会出现访问冲突等问题。
单核CPU,多线程。进程中的多个线程会同时访问进程中的共享数据,CPU将某块内存加载到缓存后,不同线程在访问相同的物理地址的时候,都会映射到相同的缓存位置,这样即使发生线程的切换,缓存仍然不会失效。但由于任何时刻只能有一个线程在执行,因此不会出现缓存访问冲突。
多核CPU,多线程。每个核都至少有一个L1 缓存。多个线程访问进程中的某个共享内存,且这多个线程分别在不同的核心上执行,则每个核心都会在各自的caehe中保留一份共享内存的缓冲。由于多核是可以并行的,可能会出现多个线程同时写各自的缓存的情况,而各自的cache之间的数据就有可能不同。
在CPU和主存之间增加缓存,在多线程场景下就可能存在缓存一致性问题,也就是说,在多核CPU中,每个核的自己的缓存中,关于同一个数据的缓存内容可能不一致,如下图:
处理器优化和指令重排
上面提到在在CPU和主存之间增加缓存,在多线程场景下会存在缓存一致性问题。除了这种情况,还有一种硬件问题也比较重要。那就是为了使处理器内部的运算单元能够尽量的被充分利用,处理器可能会对输入代码进行乱序执行处理。这就是处理器优化。
除了现在很多流行的处理器会对代码进行优化乱序处理,很多编程语言的编译器也会有类似的优化,比如Java虚拟机的即时编译器(JIT)也会做指令重排。
可想而知,如果任由处理器优化和编译器对指令重排的话,就可能导致各种各样的问题。
并发编程的问题
前面说的和硬件有关的概念你可能听得有点蒙,还不知道他到底和软件有啥关系。但是关于并发编程的问题你应该有所了解,比如原子性问题,可见性问题和有序性问题。
其实,原子性问题,可见性问题和有序性问题。是人们抽象定义出来的。而这个抽象的底层问题就是前面提到的缓存一致性问题、处理器优化问题和指令重排问题等。
这里简单回顾下这三个问题,并不准备深入展开,感兴趣的读者可以自行学习。我们说,并发编程,为了保证数据的安全,需要满足以下三个特性:
原子性:是指在一个操作中就是cpu不可以在中途暂停然后再调度,既不被中断操作,要不执行完成,要不就不执行。对于单个的读/写操作,在多线程环境下保证是原子操作,但复合操作比如i++,相当于是以下三个操作:
int temp = get(); // 读
temp += 1; // ADD
set(temp); // 写
Java主要提供了锁机制以及CAS操作实现原子性,对于单个读/写操作是通过LOCK#信号或“缓存锁定”实现的。
除此之外,long和double类型的变量读/写是非原子性的,每次都只读/写32位数据,所以一个单个的读/写操作就变成了两个读/写操作,有可能在只读/写了其中32位操作后CPU就被其他线程抢占到。
可见性:是指当多个线程访问同一个变量时,一个线程修改了这个变量的值,其他线程能够立即看得到修改的值。由于每个线程都有一个私有的工作空间,并且保存一个主存中共享变量的副本,在线程对私有的工作空间中的数据进行写操作,别的线程并没有读到最新的值,就会出现问题。Java提供了volatile关键字保证了内存的可见性,底层通过LOCK#或“缓存锁定”实现。
instance = new Singleton(); // instance 是一个volatile变量
以上代码在进行反汇编后得到的汇编代码如下:
0x01a3de1d: movb $0×0,0×1104800(%esi);
0x01a3de24: lock addl $0×0,(%esp);
如果是一个volatile关键字修饰的变量,则会有第二行的汇编代码,这是一条含有lock前缀的代码。带有lock前缀的代码则会通过LOCK#或通过“缓存锁定”实现线程间的可见性。
有序性:即程序执行的顺序按照代码的先后顺序执行。编译器和处理器会通过多种方式比如重排序对代码进行优化,然而在重排序后可能会导致运行结果与预想的不同。
a.重排序的方式:
计算机在执行程序时,为了提高性能,编译器和处理器的常常会对指令做重排,一般分以下3种:
- 编译器优化的重排序:
编译器在不改变单线程程序语义的前提下,可以重新安排语句的执行顺序。【as-if-serial原则保证,as-if-serial语义:不管怎么重排序(编译器和处理器为了提高并行度),(单线程)程序的执行结果不能被改变。】
- 指令级并行的重排序:
现代处理器采用了指令级并行技术来将多条指令重叠执行。如果不存在数据依赖性(即后一个执行的语句无需依赖前面执行的语句的结果),处理器可以改变语句对应的机器指令的执行顺序。
- 内存系统重排序:
由于处理器使用缓存和读写缓存冲区,这使得加载(load)和存储(store)操作看上去可能是在乱序执行,因为三级缓存的存在,导致内存与缓存的数据同步存在时间差。
b.内存屏障(Memory Barrier,又称内存栅栏):
我们知道一个线程是由一个CPU核来处理的,但现代CPU为了继续挖掘并行运算能力,出现了流水线技术,即每个内核有多个电路,调度器将串行指令分解为多步,并将不同指令的各步操作结果叠加,从而实现并行处理,指令的每步都由各自独立的电路来处理,最后汇总处理。这在相当程度上提高了并发能力。那么指令该如何分解?指令的前后执行顺序是否有关系?如何保证结果正确?这又是一个复杂的课题:重排序。
作为CPU来说,为了提高执行效率,允许将一段程序代码分发到各个流水线去执行,但我们的程度代码可能是有前后逻辑关系的,比如int a=10;int b=a*2;这里如果将第二行移到第一行前面执行,则产生了错误结果,这里的计算b的代码我们称之为与前面的a存在数据依赖.
下面是不同处理器对不同重排序类型的支持程度
从上面可以看到,各种处理器均不对“数据依赖”类型的语句进行重排序。那这里的一堆Load-Load、Load-Store是什么意思呢?
Load指的是从内存中读取数据的操作&#xff0c;即if(a<1)这样的语句(只读不写)&#xff1b;Store指的是向内存中写入数据的操作&#xff0c;即a&#61;10这样的语句(只写不读)。Load-Load指先读后读的操作是否允许重排序&#xff1b;Load-Store指先读后写是否允许重排序&#xff1b;Store-Store指先写后写的两个语句是否允许重排序&#xff1b;Store-Load指先写后读的两个语句是否允许重排序。
从上表可以看到&#xff0c;对于我们常用的Intel和AMD所使用的x86架构&#xff0c;只支持Store-Load重排序&#xff0c;这是因为CPU在写入的时候是首先写入寄存器&#xff0c;包括各级缓存&#xff0c;此时并没有刷新到内存中&#xff0c;如果等待其完成再读则太慢了&#xff0c;所以允许重排序。只要理解了写入是通过缓存批量执行的&#xff0c;那就不难理解。
说了这么多&#xff0c;还是没有提到内存屏障&#xff0c;它究竟是什么&#xff1f;仍然以ia64为例&#xff0c;它是允许Store-Store重排序的&#xff0c;但未必能保证在多线程环境下准确&#xff0c;比如这样的程序:
static class ThreadA extends Thread{public void run(){a&#61;1;flag&#61;true;}}static class ThreadB extends Thread{public void run(){if(flag){a&#61;a*1;}if(a&#61;&#61;0){System.out.println("ha,a&#61;&#61;0");}}}
当ThreadA执行的时候&#xff0c;按照顺序执行的逻辑&#xff0c;当flag&#61;true的时候&#xff0c;a必然等于1&#xff1b;那么ThreadB在if(flag)为真时&#xff0c;会用a&#61;1的值去计算a*1&#xff0c;但CPU对Store-Load重排序后&#xff0c;ThreadA中的a&#61;1可能会在flag&#61;true后面执行&#xff0c;如此就造成ThreadB中a&#61;a*1没有根据预期的a&#61;1来执行&#xff0c;所以操作系统需要提供一种机制&#xff0c;以禁用重排序&#xff0c;将决定是否重排序的选择权交给应用程序。如果应用程序不允许重排序&#xff0c;则插入相应的内存屏障指令将其禁用。
Java为了能在不同平台下都能正确运行&#xff0c;提出了自己的内存模型&#xff0c;其中就定义了几种内存屏障指令(间接调用的都是底层操作系统的内存屏障指令)
StoreLoad Barriers是一个“全能型”的屏障&#xff0c;它同时具有其他三个屏障的效果。现代的多处理器大都支持该屏障&#xff08;其他类型的屏障不一定被所有处理器支持&#xff09;。执行该屏障开销会很昂贵&#xff0c;因为当前处理器通常要把写缓冲区中的数据全部刷新到内存中&#xff08;buffer fully flush&#xff09;。
volatile语义中的内存屏障
- volatile的内存屏障策略非常严格保守&#xff0c;非常悲观且毫无安全感的心态&#xff1a;
在每个volatile写操作前插入StoreStore屏障&#xff0c;在写操作后插入StoreLoad屏障&#xff1b;
在每个volatile读操作前插入LoadLoad屏障&#xff0c;在读操作后插入LoadStore屏障&#xff1b;
- 由于内存屏障的作用&#xff0c;避免了volatile变量和其它指令重排序、线程之间实现了通信&#xff0c;使得volatile表现出了锁的特性。
final语义中的内存屏障
- 对于final域&#xff0c;编译器和CPU会遵循两个排序规则&#xff1a;
- 新建对象过程中&#xff0c;构造体中对final域的初始化写入和这个对象赋值给其他引用变量&#xff0c;这两个操作不能重排序&#xff1b;&#xff08;废话嘛&#xff09;
- 初次读包含final域的对象引用和读取这个final域&#xff0c;这两个操作不能重排序&#xff1b;&#xff08;晦涩&#xff0c;意思就是先赋值引用&#xff0c;再调用final值&#xff09;
内存屏障是一个CPU指令&#xff0c;Java编译器在生成指令序列的适当位置会插入内存屏障指令来禁止特定类型的处理器重排序。它的作用有两个&#xff1a;
一&#xff1a;保证特定操作的执行顺序&#xff1b;
二&#xff1a;保证某些变量的内存可见性。
如果在指令间插入一条Memory Barrier则会告诉编译器和CPU&#xff0c;不管什么指令都不能和这条Memory Barrier指令重排序&#xff0c;也就是说通过插入内存屏障禁止在内存屏障前后的指令执行重排序优化。Memory Barrier的另外一个作用是强制刷出各种CPU的缓存数据&#xff0c;因此任何CPU上的线程都能读取到这些数据的最新版本。
3.happens-before原则&#xff1a;
使用happens-before的概念来指定两个操作之间的执行顺序。由于这两个操作可以在一个线程之内&#xff0c;也可以是在不同线程之间。因此&#xff0c;JMM可以通过happens-before关系向程序员提供跨线程的内存可见性保证。
【如果A happens-before B&#xff0c;JMM并不要求A一定要在B之前执行。JMM仅仅要求A操作&#xff08;执行的结果&#xff09;对B操作可见&#xff0c;且A操作按顺序排在B操作之前。】
1&#xff09;程序顺序规则&#xff1a;一个线程中的每个操作&#xff0c;happens-before于该线程中的任意后续操作。【在A happens-before B中&#xff0c;如果A和B重排序后不会导致结果变化&#xff0c;那么这种重排序是被允许的】
2&#xff09;监视器锁规则&#xff1a;对一个锁的解锁&#xff0c;happens-before于随后对这个锁的加锁。
3&#xff09;volatile变量规则&#xff1a;对一个volatile域的写&#xff0c;happens-before于任意后续对这个volatile域的读。
4&#xff09;传递性&#xff1a;如果A happens-before B&#xff0c;且B happens-before C&#xff0c;那么A happens-before C。
5&#xff09;start()规则&#xff1a;如果线程A执行操作ThreadB.start()&#xff08;启动线程B&#xff09;&#xff0c;那么A线程的ThreadB.start()操作happens-before于线程B中的任意操作。
6&#xff09;join()规则&#xff1a;如果线程A执行操作ThreadB.join()并成功返回&#xff0c;那么线程B中的任意操作 happens-before于线程A从ThreadB.join()操作成功返回。
有没有发现&#xff0c;缓存一致性问题其实就是可见性问题。而处理器优化是可以导致原子性问题的。指令重排即会导致有序性问题。所以&#xff0c;后文将不再提起硬件层面的那些概念&#xff0c;而是直接使用大家熟悉的原子性、可见性和有序性。
什么是内存模型
前面提到的&#xff0c;缓存一致性问题、处理器器优化的指令重排问题是硬件的不断升级导致的。那么&#xff0c;有没有什么机制可以很好的解决上面的这些问题呢&#xff1f;
最简单直接的做法就是废除处理器和处理器的优化技术、废除CPU缓存&#xff0c;让CPU直接和主存交互。但是&#xff0c;这么做虽然可以保证多线程下的并发问题。但是&#xff0c;这就有点因噎废食了。
所以&#xff0c;为了保证并发编程中可以满足原子性、可见性及有序性。有一个重要的概念&#xff0c;那就是——内存模型。
为了保证共享内存的正确性&#xff08;可见性、有序性、原子性&#xff09;&#xff0c;内存模型定义了共享内存系统中多线程程序读写操作行为的规范。通过这些规则来规范对内存的读写操作&#xff0c;从而保证指令执行的正确性。它与处理器有关、与缓存有关、与并发有关、与编译器也有关。他解决了CPU多级缓存、处理器优化、指令重排等导致的内存访问问题&#xff0c;保证了并发场景下的一致性、原子性和有序性。
内存模型解决并发问题主要采用两种方式&#xff1a;限制处理器优化和使用内存屏障。本文就不深入底层原理来展开介绍了&#xff0c;感兴趣的朋友可以自行学习。
什么是Java内存模型
前面介绍过了计算机内存模型&#xff0c;这是解决多线程场景下并发问题的一个重要规范。那么具体的实现是如何的呢&#xff0c;不同的编程语言&#xff0c;在实现上可能有所不同。
我们知道&#xff0c;Java程序是需要运行在Java虚拟机上面的&#xff0c;Java内存模型&#xff08;Java Memory Model ,JMM&#xff09;就是一种符合内存模型规范的&#xff0c;屏蔽了各种硬件和操作系统的访问差异的&#xff0c;保证了Java程序在各种平台下对内存的访问都能保证效果一致的机制及规范。
提到Java内存模型&#xff0c;一般指的是JDK 5 开始使用的新的内存模型&#xff0c;主要由JSR-133: JavaTM Memory Model and Thread Specification 描述。感兴趣的可以参看下这份PDF文档&#xff08;http://www.cs.umd.edu/~pugh/java/memoryModel/jsr133.pdf&#xff09;
Java内存模型规定了所有的变量都存储在主内存中&#xff0c;每条线程还有自己的工作内存&#xff0c;线程的工作内存中保存了该线程中是用到的变量的主内存副本拷贝&#xff0c;线程对变量的所有操作都必须在工作内存中进行&#xff0c;而不能直接读写主内存。不同的线程之间也无法直接访问对方工作内存中的变量&#xff0c;线程间变量的传递均需要自己的工作内存和主存之间进行数据同步进行。
而JMM就作用于工作内存和主存之间数据同步过程。他规定了如何做数据同步以及什么时候做数据同步。
这里面提到的主内存和工作内存&#xff0c;读者可以简单的类比成计算机内存模型中的主存和缓存的概念。特别需要注意的是&#xff0c;主内存和工作内存与JVM内存结构中的Java堆、栈、方法区等并不是同一个层次的内存划分&#xff0c;无法直接类比。《深入理解Java虚拟机》中认为&#xff0c;如果一定要勉强对应起来的话&#xff0c;从变量、主内存、工作内存的定义来看&#xff0c;主内存主要对应于Java堆中的对象实例数据部分。工作内存则对应于虚拟机栈中的部分区域。
所以&#xff0c;再来总结下&#xff0c;JMM是一种规范&#xff0c;目的是解决由于多线程通过共享内存进行通信时&#xff0c;存在的本地内存数据不一致、编译器会对代码指令重排序、处理器会对代码乱序执行等带来的问题。
Java内存模型的实现
了解Java多线程的朋友都知道&#xff0c;在Java中提供了一系列和并发处理相关的关键字&#xff0c;比如volatile
、synchronized
、final
、concurren
包等。其实这些就是Java内存模型封装了底层的实现后提供给程序员使用的一些关键字。
在开发多线程的代码的时候&#xff0c;我们可以直接使用synchronized
等关键字来控制并发&#xff0c;从来就不需要关心底层的编译器优化、缓存一致性等问题。所以&#xff0c;Java内存模型&#xff0c;除了定义了一套规范&#xff0c;还提供了一系列原语&#xff0c;封装了底层实现后&#xff0c;供开发者直接使用。
本文并不准备把所有的关键字逐一介绍其用法&#xff0c;因为关于各个关键字的用法&#xff0c;网上有很多资料。读者可以自行学习。本文还有一个重点要介绍的就是&#xff0c;我们前面提到&#xff0c;并发编程要解决原子性、有序性和一致性的问题&#xff0c;我们就再来看下&#xff0c;在Java中&#xff0c;分别使用什么方式来保证。
原子性
在Java中&#xff0c;为了保证原子性&#xff0c;提供了两个高级的字节码指令monitorenter
和monitorexit
。在synchronized的实现原理文章中&#xff0c;介绍过&#xff0c;这两个字节码&#xff0c;在Java中对应的关键字就是synchronized
。
因此&#xff0c;在Java中可以使用synchronized
来保证方法和代码块内的操作是原子性的。
可见性
Java内存模型是通过在变量修改后将新值同步回主内存&#xff0c;在变量读取前从主内存刷新变量值的这种依赖主内存作为传递媒介的方式来实现的。
Java中的volatile
关键字提供了一个功能&#xff0c;那就是被其修饰的变量在被修改后可以立即同步到主内存&#xff0c;被其修饰的变量在每次是用之前都从主内存刷新。因此&#xff0c;可以使用volatile
来保证多线程操作时变量的可见性。
除了volatile
&#xff0c;Java中的synchronized
和final
两个关键字也可以实现可见性。只不过实现方式不同&#xff0c;这里不再展开了。
有序性
在Java中&#xff0c;可以使用synchronized
和volatile
来保证多线程之间操作的有序性。实现方式有所区别&#xff1a;
volatile
关键字会禁止指令重排。synchronized
关键字保证同一时刻只允许一条线程操作。
好了&#xff0c;这里简单的介绍完了Java并发编程中解决原子性、可见性以及有序性可以使用的关键字。读者可能发现了&#xff0c;好像synchronized
关键字是万能的&#xff0c;他可以同时满足以上三种特性&#xff0c;这其实也是很多人滥用synchronized
的原因。
但是synchronized
是比较影响性能的&#xff0c;虽然编译器提供了很多锁优化技术&#xff0c;但是也不建议过度使用。
Java的内存结构
Java的内存结构&#xff0c;也就是运行时的数据区域&#xff1b;
Java虚拟机在执行Java程序的过程中&#xff0c;会把它管理的内存划分为几个不同的数据区域&#xff0c;这些区域都有各自的用途、创建时间、销毁时间。
Java运行时数据区分为下面几个内存区域&#xff1a;
1.PC寄存器/程序计数器&#xff1a;
严格来说是一个数据结构&#xff0c;用于保存当前正在执行的程序的内存地址&#xff0c;由于Java是支持多线程执行的&#xff0c;所以程序执行的轨迹不可能一直都是线性执行。当有多个线程交叉执行时&#xff0c;被中断的线程的程序当前执行到哪条内存地址必然要保存下来&#xff0c;以便用于被中断的线程恢复执行时再按照被中断时的指令地址继续执行下去。为了线程切换后能恢复到正确的执行位置&#xff0c;每个线程都需要有一个独立的程序计数器&#xff0c;各个线程之间计数器互不影响&#xff0c;独立存储&#xff0c;我们称这类内存区域为“线程私有”的内存,这在某种程度上有点类似于“ThreadLocal”&#xff0c;是线程安全的。
2.Java栈 Java Stack&#xff1a;
Java栈总是与线程关联在一起的&#xff0c;每当创建一个线程&#xff0c;JVM就会为该线程创建对应的Java栈&#xff0c;在这个Java栈中又会包含多个栈帧(Stack Frame)&#xff0c;这些栈帧是与每个方法关联起来的&#xff0c;每运行一个方法就创建一个栈帧&#xff0c;每个栈帧会含有一些局部变量、操作栈和方法返回值等信息。每当一个方法执行完成时&#xff0c;该栈帧就会弹出栈帧的元素作为这个方法的返回值&#xff0c;并且清除这个栈帧&#xff0c;Java栈的栈顶的栈帧就是当前正在执行的活动栈&#xff0c;也就是当前正在执行的方法&#xff0c;PC寄存器也会指向该地址。只有这个活动的栈帧的本地变量可以被操作栈使用&#xff0c;当在这个栈帧中调用另外一个方法时&#xff0c;与之对应的一个新的栈帧被创建&#xff0c;这个新创建的栈帧被放到Java栈的栈顶&#xff0c;变为当前的活动栈。同样现在只有这个栈的本地变量才能被使用&#xff0c;当这个栈帧中所有指令都完成时&#xff0c;这个栈帧被移除Java栈&#xff0c;刚才的那个栈帧变为活动栈帧&#xff0c;前面栈帧的返回值变为这个栈帧的操作栈的一个操作数。
由于Java栈是与线程对应起来的&#xff0c;Java栈数据不是线程共有的&#xff0c;所以不需要关心其数据一致性&#xff0c;也不会存在同步锁的问题。
在Java虚拟机规范中&#xff0c;对这个区域规定了两种异常状况&#xff1a;如果线程请求的栈深度大于虚拟机所允许的深度&#xff0c;将抛出StackOverflowError异常&#xff1b;如果虚拟机可以动态扩展&#xff0c;如果扩展时无法申请到足够的内存&#xff0c;就会抛出OutOfMemoryError异常。在Hot Spot虚拟机中&#xff0c;可以使用-Xss参数来设置栈的大小。栈的大小直接决定了函数调用的可达深度。
3.堆 Heap:
堆是JVM所管理的内存中国最大的一块&#xff0c;是被所有Java线程锁共享的&#xff0c;不是线程安全的&#xff0c;在JVM启动时创建。堆是存储Java对象的地方&#xff0c;这一点Java虚拟机规范中描述是&#xff1a;所有的对象实例以及数组都要在堆上分配。Java堆是GC管理的主要区域&#xff0c;从内存回收的角度来看&#xff0c;由于现在GC基本都采用分代收集算法&#xff0c;所以Java堆还可以细分为&#xff1a;新生代和老年代&#xff1b;新生代再细致一点有Eden空间、From Survivor空间、To Survivor空间等。
4.方法区Method Area:
方法区存放了要加载的类的信息&#xff08;名称、修饰符等&#xff09;、类中的静态常量、类中定义为final类型的常量、类中的Field信息、类中的方法信息&#xff0c;当在程序中通过Class对象的getName.isInterface等方法来获取信息时&#xff0c;这些数据都来源于方法区。方法区是被Java线程锁共享的&#xff0c;不像Java堆中其他部分一样会频繁被GC回收&#xff0c;它存储的信息相对比较稳定&#xff0c;在一定条件下会被GC&#xff0c;当方法区要使用的内存超过其允许的大小时&#xff0c;会抛出OutOfMemory的错误信息。方法区也是堆中的一部分&#xff0c;就是我们通常所说的Java堆中的永久区 Permanet Generation&#xff0c;大小可以通过参数来设置,可以通过-XX:PermSize指定初始值&#xff0c;-XX:MaxPermSize指定最大值。
5.常量池Constant Pool:
常量池本身是方法区中的一个数据结构。常量池中存储了如字符串、final变量值、类名和方法名常量。常量池在编译期间就被确定&#xff0c;并保存在已编译的.class文件中。一般分为两类&#xff1a;字面量和应用量。字面量就是字符串、final变量等。类名和方法名属于引用量。引用量最常见的是在调用方法的时候&#xff0c;根据方法名找到方法的引用&#xff0c;并以此定为到函数体进行函数代码的执行。引用量包含&#xff1a;类和接口的权限定名、字段的名称和描述符&#xff0c;方法的名称和描述符。
6.本地方法栈Native Method Stack:
本地方法栈和Java栈所发挥的作用非常相似&#xff0c;区别不过是Java栈为JVM执行Java方法服务&#xff0c;而本地方法栈为JVM执行Native方法服务。本地方法栈也会抛出StackOverflowError和OutOfMemoryError异常。
主内存和工作内存&#xff1a;
Java内存模型的主要目标是定义程序中各个变量的访问规则&#xff0c;即在JVM中将变量存储到内存和从内存中取出变量这样的底层细节。此处的变量与Java编程里面的变量有所不同步&#xff0c;它包含了实例字段、静态字段和构成数组对象的元素&#xff0c;但不包含局部变量和方法参数&#xff0c;因为后者是线程私有的&#xff0c;不会共享&#xff0c;当然不存在数据竞争问题&#xff08;如果局部变量是一个reference引用类型&#xff0c;它引用的对象在Java堆中可被各个线程共享&#xff0c;但是reference引用本身在Java栈的局部变量表中&#xff0c;是线程私有的&#xff09;。为了获得较高的执行效能&#xff0c;Java内存模型并没有限制执行引起使用处理器的特定寄存器或者缓存来和主内存进行交互&#xff0c;也没有限制即时编译器进行调整代码执行顺序这类优化措施。
JMM规定了所有的变量都存储在主内存&#xff08;Main Memory&#xff09;中。每个线程还有自己的工作内存&#xff08;Working Memory&#xff09;,线程的工作内存中保存了该线程使用到的变量的主内存的副本拷贝&#xff0c;线程对变量的所有操作&#xff08;读取、赋值等&#xff09;都必须在工作内存中进行&#xff0c;而不能直接读写主内存中的变量&#xff08;volatile变量仍然有工作内存的拷贝&#xff0c;但是由于它特殊的操作顺序性规定&#xff0c;所以看起来如同直接在主内存中读写访问一般&#xff09;。不同的线程之间也无法直接访问对方工作内存中的变量&#xff0c;线程之间值的传递都需要通过主内存来完成。
线程1和线程2要想进行数据的交换一般要经历下面的步骤&#xff1a;
1.线程1把工作内存1中的更新过的共享变量刷新到主内存中去。
2.线程2到主内存中去读取线程1刷新过的共享变量&#xff0c;然后copy一份到工作内存2中去。
总结
在读完本文之后&#xff0c;相信你应该了解了什么是Java内存模型、Java内存模型的作用以及Java中内存模型做了什么事情等。关于Java中这些和内存模型有关的关键字&#xff0c;希望读者还可以继续深入学习&#xff0c;并且自己写几个例子亲自体会一下。
可以参考《深入理解Java虚拟机》和《Java并发编程的艺术》两本书。
出处&#xff1a;https://www.hollischuang.com/archives/2550
https://www.cnblogs.com/lewis0077/p/5143268.html