一、java内存模型
引言:JVM定义了一种Java内存模型(Java Memory Model,JMM)来屏蔽掉各种硬件和操作系统的内存访问差异,以实现让Java程序在各种平台下都能达到一致的内存访问效果。在此之前,C/C++直接使用物理硬件和操作系统的内存模型,因此,会由于不同平台下的内存模型的差异,有可能导致程序在一套平台上并发完全正常,而在另一套平台上并发访问经常出错。
主内存与工作内存:
- Java内存模型规定所有的变量都是存在于主存中的,每个线程都有自己的工作内存。线程对变量的操作都必须在工作内存中进行,而不能直接对主存进行操作,并且每个线程不能访问其他线程的工作内存。
- Java内存模型的主要目标是定义程序中各个变量的访问规则,即在JVM中将变量存储到内存和从内存中取出变量这样的底层细节。此处的变量包括实例字段、静态字段和构成数组对象的元素,但不包括局部变量和方法参数,因为后两者是线程私有的,不会被线程共享
- Java内存模型规定了所有的变量都存储在主内存中。每条线程还有自己的工作内存,线程的工作内存中保存了被该线程使用到的变量的主内存副本拷贝,线程对变量的所有操作(读取、赋值等)都必须在工作内存进行,而不能直接读写主内存中的变量。不同的线程之间也无法直接访问对方工作内存中的变量,线程间变量值的传递均需要通过主内存来完成。线程、主内存、工作内存三者的交互关系如下所示
java内存模型的三大特性: - 原子性 : 由Java内存模型来直接保证的原子性变量操作包括read、load、assign、use、store和read。大致可以认为,基本数据类型的访问读写是具备原子性的。如若需要更大范围的原子性,需要synchronized关键字约束。(即一个操作或者多个操作 要么全部执行并且执行的过程不会被任何因素打断,要么就都不执行)
- 可见性 : 可见性是指当一个线程修改了共享变量的值,其他线程能够立即得知这个修改。volatile、synchronized、final三个关键字可以实现可见性。
- 有序性 : 如果在本线程内观察,所有的操作都是有序的;如果在线程中观察另外一个线程,所有的操作都是无序的。前半句是指"线程内表现为串行",后半句是指"指令重排序"和"工作内存与主内存同步延迟"现象。
volatile型变量:
- volatile是java虚拟机提供的最轻量级的同步机制,它具有可见性和有序性,但不保证原子性,在大多数场景下,volatile的总开销仍然比锁要低。volatile是强制从主内存(公共堆)中取得变量的值,而不是从线程的工作内存(私有堆栈)中取得变量的值。如下图所示
- volatile保证了变量的新值能立即同步到主内存,以及每次使用之前立即从主内存刷新。因此可以说volatile保证了多线程操作时变量的可见性,而普通变量不能保证这一点。volatile 也可以保证禁止重排序。
二、运行时数据区域:
JVM会在执行Java程序的过程中把它管理的内存划分为若干个不同的数据区域。这些数据区域各有各的用处,各有各的创建与销毁时间,有的区域随着JVM进程的启动而存在,有的区域则依赖用户线程的启动和结束而创建与销毁。一般来说,JVM所管理的内存将会包含以下几个运行时数据区域
线程私有区域:程序计数器、Java虚拟机栈、本地方法栈
线程共享区域:方法区,堆java堆,运行时常量池
1.程序计数器(线程私有)
程序计数器是一个比较小的内存空间,字节码解释器工作时,通过改变这个计数器的值来选取下一条需要执行的字节码指令。分支、循环、跳转、异常处理、线程恢复等基础功能都需要依赖这个计数器来完成。多线程中,为了让线程切换后能恢复到正确的执行位置,每个线程都需要有一个自己独立的程序计数器,各个线程之间互不影响、独立存储,因此这块内存是线程私有的。
程序计数器有两个作用:(1)字节码解释器通过改变程序计数器来一次读取指令,从而实现代码的流程控制,比如我们常见的顺序、循环、选择、异常处理等。(2)在多线程的情况下,程序计数器用来记录当前线程执行的位置,当线程切换回来的时候仍然可以知道该线程上次执行到了哪里。而且程序计数器是唯一一个不会出现OutOfMeroryError的内存区域。
2.Java虚拟机栈(线程私有)
描述Java方法运行过程的内存模型,Java虚拟机栈会为每一个即将执行的方法创建一个叫做“栈帧”的区域,该区域用来存储该方法运行时需要的一些信息,包括:局部变量表、操作数栈、动态链接、方法出口等信息。比如我们方法执行过程中需要创建变量时,就会将局部变量插入到局部变量表中,局部变量的运算、传递等在操作数栈中进行,当方法执行结束后,这个方法对应的栈帧将出栈,并释放内存空间。栈中常会发生的两种异常,StackOverFlowError和OutOfMemoryError。
StackOverFlowError表示当前线程申请的栈超过了事先定好的栈的最大深度,但内存空间可能还有很多。 而OutOfMemoryError是指当线程申请栈时发现栈已经满了,而且内存也全都用光了。
3.本地方法栈(线程私有)
结构上和Java虚拟机栈一样,只不过Java虚拟机栈是运行Java方法的区域,而本地方法栈是运行本地方法的内存模型。运行本地方法时也会创建栈帧,同样栈帧里也有局部变量表、操作数栈、动态链接和方法出口等信息,在本地方法执行结束后栈帧也会出栈并释放内存资源,也会发生OutOfMemoryError异常。
4.java堆(线程共享)
Java堆(Java Heap)是JVM所管理的最大内存区域。Java堆是所有线程共享的一块区域,在JVM启动时创建。此内存区域存放的都是对象实例。JVM规范中说到:“所有的对象实例以及数组都要在堆上分配”。
Java堆是垃圾回收器管理的主要区域,因此很多时候可以称之为"GC堆"。根据JVM规范规定的内容,Java堆可以处于物理上不连续的内存空间中。Java堆在主流的虚拟机中都是可扩展的(-Xmx设置最大值,-Xms设置最小值)。如果在堆中没有足够的内存完成实例分配并且堆也无法再拓展时,将会抛出OMM
5.方法区(线程共享)
主要用来存放类信息、类的静态变量、常量、运行时常量池、即时编译器编译后的代码等数据,方法区的大小是可以动态扩展的。方法区是个线程共享的内存区域,当方法区无法满足内存分配需求时,将抛出OutOfMemoryError 异常。
运行时常量池:是方法区的一部分,存放字面量与符号引用。
- 字面量:字符串(JDK1.7后移动到堆中)、final常量、 基本数据类型的值。
- 符号引用:类和结构的完全限定名、字段的名称和描述符、方法的名称和描述符。
总结: 方法区和堆都是线程共享的,在JVM启动时创建,在JVM停止时销毁,而Java虚拟机栈、本地方法栈、程序计数器是线程私有的,随线程的创建而创建,随线程的结束而死亡。