java内存模型与内存区域_Java内存模型、内存区域

内存模型

为什么需要内存模型

首先,我们知道绝大多数的运算任务都不可能只靠处理器“计算”就能完成,处理器至少需要与内存交互,如读取运算数据、存储运算结果等,这个I/O操作是很难消除的(无法仅靠寄存器来完成所以运算任务)。由于计算机的存储设备与处理器的运算速度有几个数量级的差距,所以现代计算机系统不得不加入一层读写速度尽可能接近处理器的高速缓存(Cache)来作为内存与处理器之间的缓冲:将运算需要使用到的数据复制到缓冲中,让运算能快速进行,当运算借宿后再从缓冲同步回内存之中,这样处理器就无需等待缓慢的内存读写了。

虽然高速缓存很好的处理了处理器与内存之间的速度矛盾,但也引入了一个新问题:缓存一致性(Cache Coherence)。在多处理器系统中,每个处理器都有自己的高速缓存,而他们又共享同一主内存,所以当他们运算任务都涉及同一块主内存区域时,而他们各自的缓冲数据不一致时,同步回主内存会造成数据不一致问题。

8dcbe5da2779beccbb47cfc874b0e5a0.png

为了解决一致性问题,需要各个处理器访问缓存时都遵循一些协议,读写时根据协议来进行操作。内存模型可以理解成在特定的操作协议下,对特定的内存或高速缓存进行读写访问的过程抽象。不同架构的物理机器可以拥有不一样的内存模型,而Java虚拟机也有自己的内存模型。

98fa52028e63039af36bdfab197a06b6.png

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

原子性、可见性和有序性

原子性

在一个操作中,CPU不可以在中途停止然后再调度,即不被中断操作,要么执行完成,要么不执行

可见性

当多线程访问同一个变量,一个线程修改了该变量的值,其他线程可立即看到修改的值

有序性

程序的执行按照代码的先后顺序执行

Java内存模型

Java内存模型的主要目标是定义程序中各个变量的访问规则,即在虚拟机中将变量存储到内存和从内存取出变量的底层细节。Java内存模型规定所以变量都存储在主内存中(虚拟机内存的一部分)。每天线程都有自己的工作内存(类似高速缓存),工作内存保存该线程用到的变量的主内存副本拷贝,线程对变量的所有操作都在工作内存中进行。这里讲的主内存、工作内存与Java内存区域中的Java堆、栈、方法区等不是同一个层次的内存划分。如果一定要勉强对应起来,从定义来看,主内存主要对应Java堆中对象实例数据部分,工作内存对应虚拟机栈中部分区域。

内存交互操作

Java内存模型定义了8中操作来完成一个变量从主内存拷贝到工作内存和从工作内存同步回主内存,这些操作都是原子的、不可再分的。

lock:作用于主内存的变量,将变量标识为线程独占状态

unlock:作用于主内存变量,将锁定状态变量释放

read:作用于主内存变量,把变量值从主内存传输到线程工作内存中,以便load动作使用

load:作用于工作内存变量,将read操作得到的变量值放入工作内存的变量副本中

use:作用于工作内存变量,把工作内存中变量值传递给执行引擎,每当虚拟机遇到需要使用变量值的字节码指令将执行该动作。

assign:作用于工作内存,把从执行引擎收到的值赋给工作内存的变量,每当虚拟机遇到给变量赋值的字节码执行将执行该动作。

store:作用于工作内存的变量,把工作内存变量值传送给主内存,以便write操作使用。

write:作用于主内存的变量,把store操作的值放入主内存的变量中。

volatile

volatile具备两种特性,第一是保证此变量对所有线程的可见性,第二是禁止指令重排序优化。

在对volatile做修改时相当于对变量做了内存交互操作的store和write操作,由此对其他CPU立即可见。而禁止指令重排序优化是通过内存屏障来实现,即重排序时不能把后面的指令重排序到内存屏障之前的位置。

通过这8中内存访问操作、操作规则限定和volatile的一些特殊规定,就可以确定Java程序中哪些内存访问操作在并发下是安全的。

内存区域

9d459c18134a653340b406c5d20f3949.png

56956526831298e1ccf0f00b7ed16b1d.png

10717764.html

10717764.html

程序计数器

可理解为字节码行号指示器,线程私有,当CPU执行某个线程某个类时,执行一段字节码后CPU调度其他线程,该线程停止,之后又回到该线程继续执行,根据程序计数器可从之前停止的位置继续执行。

虚拟机栈

描述Java方法执行的内存模型:每个方法在执行时都会创建一个栈帧(stack frame)用于存储局部变量表、操作数栈、动态链接、方法出口等信息。每一个方法从调用到执行完成的过程,就对应着一个栈帧在虚拟机栈中入栈到出栈的过程。

1.局部变量表

局部变量表是一组局部变量值存储空间,用于存放方法参数和方法内部定义的局部变量。在Java文件编译为Class文件时,就在方法表的Code属性的max_locals数据项中确定了该方法需要分配的最大局部变量表的容量。

2.操作数栈

操作数栈也常被称为操作栈,它是一个后入先出栈。JVM底层字节码指令集是基于栈类型的,所有的操作码都是对操作数栈上的数据进行操作,对于每一个方法的调用,JVM会建立一个操作数栈,以供计算使用。和局部变量一样。操作数栈的最大深度也是编译的时候写入到方法表的code属性的max_stacks数据项中。操作数栈的每一个元素可以是任意的Java数据类型,包括long、double。32位数据类型所占的栈容量为1,64位数据类型所占的栈容量为2。栈容量的单位为“字宽”,对于32位虚拟机来说,一个“字宽”占4个字节,64位虚拟机来说,一个“字宽”占8个字节。当一个方法刚刚执行的时候,这个方法的操作数栈是空的,在方法执行的过程中,会有各种字节码指向操作数栈中写入和提取值,也就是入栈与出栈操作。例如,在做算术运算的时候就是通过操作数栈来进行的,又或者调用其它方法的时候是通过操作数栈来行参数传递的。 另外,在概念模型中,两个栈帧作为虚拟机栈的元素,相互之间是完全独立的,但是大多数虚拟机的实现里都会作一些优化处理,令两个栈帧出现一部分重叠。让下栈帧的部分操作数栈与上面栈帧的部分局部变量表重叠在一起,这样在进行方法调用返回时就可以共用一部分数据,而无须进行额外的参数复制传递了。

3.动态连接

每个栈帧都包含一个指向运行时常量池中该栈帧所属性方法的引用,持有这个引用是为了支持方法调用过程中的动态连接。在Class文件的常量池中存有大量的符号引用,字节码中的方法调用指令就以常量池中指向方法的符号引用为参数。这些符号引用一部分会在类加载阶段或第一次使用的时候转化为直接引用,这种转化称为静态解析。另外一部分将在每一次的运行期期间转化为直接引用,这部分称为动态连接。

4.方法返回地址

当一个方法被执行后,有两种方式退出这个方法。第一种方式是执行引擎遇到任意一个方法返回的字节码指令,这时候可能会有返回值传递给上层的方法调用者(调用当前方法的的方法称为调用者),是否有返回值和返回值的类型将根据遇到何种方法返回指令来决定,这种退出方法方式称为正常完成出口(Normal Method Invocation Completion)。另外一种退出方式是,在方法执行过程中遇到了异常,并且这个异常没有在方法体内得到处理,无论是Java虚拟机内部产生的异常,还是代码中使用athrow字节码指令产生的异常,只要在本方法的异常表中没有搜索到匹配的异常处理器,就会导致方法退出,这种退出方式称为异常完成出口(Abrupt Method Invocation Completion)。一个方法使用异常完成出口的方式退出,是不会给它的调用都产生任何返回值的。     无论采用何种方式退出,在方法退出之前,都需要返回到方法被调用的位置,程序才能继续执行,方法返回时可能需要在栈帧中保存一些信息,用来帮助恢复它的上层方法的执行状态。一般来说,方法正常退出时,调用者PC计数器的值就可以作为返回地址,栈帧中很可能会保存这个计数器值。而方法异常退出时,返回地址是要通过异常处理器来确定的,栈帧中一般不会保存这部分信息。 方法退出的过程实际上等同于把当前栈帧出栈,因此退出时可能执行的操作有:恢复上层方法的局部变量表和操作数栈,把返回值(如果有的话)压入调用都栈帧的操作数栈中,调用PC计数器的值以指向方法调用指令后面的一条指令等。

5.附加信息

虚拟机规范允许具体的虚拟机实现增加一些规范里没有描述的信息到栈帧中,例如与高度相关的信息,这部分信息完全取决于具体的虚拟机实现。在实际开发中,一般会把动态连接,方法返回地址与其它附加信息全部归为一类,称为栈帧信息。

本地方法栈

与虚拟机类似,不同是虚拟机栈为虚拟机执行Java方法(也就是字节码)服务,而本地方法栈则为Native方法服务

Java堆

所有的对象实例以及数组都在堆上分配,垃圾收集器管理的主要区域,也称为“GC堆”。Java堆可以处于物理上不连续的的内存空间,只要逻辑上是连续即可。

方法区

存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。HotSpot设计团队选择把GC分代收集扩展到方法区,或者说用永久带实现方法区,GC可以管理这部分内存。JDK1.8后用元空间metaspace实现,元空间直接操作于内存。

运行时常量池

存放编译期生成的各种字面量和符号引用。JDK1.7及之后版本的 JVM 已经将运行时常量池从方法区中移了出来,在 Java 堆(Heap)中开辟了一块区域存放运行时常量池。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值