Java内存模型(JMM)
Posted 雾远望
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Java内存模型(JMM)相关的知识,希望对你有一定的参考价值。
前言
Java内存模型,全称Java Memory Model(JMM)。
我们都知道创建一个对象需要分配内存空间并且在不需要该对象时及时回收内存。仔细回想,我们似乎并没有为我们new创建的每一个对象来编写过分配内存空间及回收内存相应的代码。但对象仍然很好的被回收了,原因是Java程序将内存的控制权交给了JVM虚拟机,所以在不了解虚拟机的内存机制的情况下,如果出现了内存泄漏与溢出,那么排查错误将是一个非常艰巨的任务。
要想知道JVM如何规划内存,首先要知道数据在内存中如何储存,当然不同类型的数据在内存中所存储的区域也不同。那么我们先带大家了解一下不同数据在内存中所储存的相应位置。
运行时数据区域划分
JVM在执行Java程序时会将它管理的内存划分为不同的数据区域。而这些区域在JDK1.8前后又有所变化。
- JDK1.8前:
线程共享 Heap堆区、Method Area方法区 线程私有 程序计数器、虚拟机栈、本地方法栈 -
JDK1.8后:
线程私有 Heap堆区、MetaSpace元空间 线程私有 程序计数器、虚拟机栈、本地方法栈
线程私有
程序计数器
程序计数器是用于存放下一条指令所在单元的地址的地方。每执行一条指令,程序计数器就会加一。每个线程都会维护一个独立的程序计数器且各线程之间的程序计数器互不影响,在程序执行过程中,线程会不断的切换,独立的线程计数器保证了当前线程的正确执行位置。程序计数器是唯一一个不会出现OutOfMemoryError的内存区域,它随着线程的创建而创建,随着线程的结束而消亡。
Java虚拟机栈
Java虚拟机栈是线程私有的,它是由许多栈帧组成,而每个栈帧又包括了局部变量表、操作数栈、动态链接以及方法出口信息。每次方法调用都会将对应的栈帧压入虚拟机栈,当方法调用结束(方法调用return或者方法抛出异常)又会将该栈帧从虚拟机栈中弹出。由于栈的特性(FILO),每次操作的都是栈顶栈帧,又被称为“当前活动栈帧”,代表当前正在执行的方法。在JVM执行引擎运行时,所有指令都针对于当前活动栈帧进行操作。
Java虚拟机栈会出现的错误:
StackOverFlowError | jvm规定了虚拟机栈的最大深度,当执行当前线程时栈帧压入的深度大于了规定的深度,就会抛出该错误,一般是由于递归导致的无限嵌套调用递归方法。 |
OutOfMemoryError | JVM的内存大小可以动态扩展,如果虚拟机在扩展栈时无法申请到足够的内存空间,就会抛出该错误。 |
本地方法栈
native关键字修饰的方法被称为本地方法,当线程调用本地方法时,会在本地方法栈中压入当前本地方法的栈帧。该栈帧中包含本地方法的局部变量表、操作数栈、动态链接、方法出口信息。当方法执行完毕时,栈帧会从本地方法栈中弹出,与虚拟机栈相同也会出现StackOverFlowError与OutOfMemoryError错误。
线程共享
堆
堆区是JVM所管理的内存中最大的一块区域,该区域被所有线程共享,堆区用于存放了大部分对象实例以及数组,随着 JIT编译器的发展与逃逸分析技术逐渐成熟,栈上分配、标量替换优化技术将会导致一些微妙的变化,从 JDK 1.7 开始已经默认开启逃逸分析,如果某些方法中的对象引用没有被返回或者未被外面使用(未逃逸出去),那么这部分对象可以直接在栈上分配内存。
Heap堆区又分为新生代和老年代,Heap堆区是垃圾收集器GC管理的主要区域。从垃圾回收的角度来看,垃圾收集器基本采用分代收集垃圾的思想,所有JVM的堆区往往采用分代划分的思想。
分代划分目的:更好的回收与分配内存
方法区与元空间
元空间是用于存放类信息、常量、静态变量、JIT即时编译器编译后的机器代码等数据。在JDK1.6时,HotSpot JVM采用Method Area方法区来储存这些数据,也叫永久代(持久代)。
方法区与永久代(持久代)的区别:
- 方法区是JVM的规范,永久代(持久代)是JVM规范的一种实现
- 只有HotSpot JVM有HotSpot JVM,对于其他类型的虚拟机例如J9(IBM)、JRockit(Oracle)都没有
- 方法区是连续的堆空间,当加载的类信息容量超过了最大可分配空间,会引发OutOfMemoryError错误,永久代(持久代)的GC与老年代捆绑,只要其中一个内存空间不足,就会触发永久代(持久代)与老年代的垃圾收集
JDK1.7时将字符串常量池、静态变量转移到了堆区
JDK1.8时采用MetaSpace代替了永久代(持久代)
元空间与永久代(持久代):
相同点 | 都是对JVM规范方法区的一种实现 |
不同点 | 永久代(持久代)在虚拟机中,元空间在本地内存 |
永久代(持久代)内存受永久代(持久代)的GC与老年代的内存空间限制,元空间大小受本地内存限制 |
Java8后HotSpot JVM为什么要删除永久代(持久代)?
- 由于永久代(持久代)内存受限范围较小,经常会发生内存溢出
- 由于JRockit VM没有永久代(持久代),移除HotSpot JVM的永久代(持久代)可以促进HotSpot JVM与JRockit VM的融合
重新认识 Java 内存模型(JMM)
通过学习《深入理解Java虚拟机》有关Java 内存模型的介绍,整理的学习笔记,供你参考。
文章目录
Java 内存模型定义
Java 内存模型 (Java Memory Model ),简称JMM屏蔽各种硬件和操作系统的内存访问的差异 性,以实现JAVA 程序在各个平台下都能达到一致的内存访问效果。
首先,引入《深入理解Java虚拟机》中的一张图,如下:
如图所示,在每一个线程中,都会有一块内部的工作内存(working memory)。这块内存保存了主内存共享数据的拷贝副本。
注意区分:JVM内存中有一块线程独享的内存空间 ----- 虚拟机栈,这里的的工作内存不是虚拟机栈。在Java线程中,不存在所谓的工作内存,它只是对CPU寄存器和高速缓存的抽象的描述。
主内存和工作内存
- Java内存模型中规定了所有的变量都存储在主内存中。
- 每条线程还有自己的工作内存,保存了该线程中使用的变量的主内存副本,线程对变量的所有操作(读取、赋值)都在工作内存中进行。
- 不同的线程之间也无法直接直接访问对方工作内存中的变量,线程间变量值的传递均需要通过主内存来完成。
- 主内存直接对应于物理硬件的内存,而为了获取更好的运行速度,虚拟机可能会让工作内存优先存储于寄存器和高速缓存中,因为程序运行时主要访问的是工作内存。
CPU普及
做为Java程序员,应该都知道线程是CPU调度的最小单位,线程中的字节码指令最终都是在CPU中执行的。
为了“压榨”处理性能,达到“高并发”的效果,在CPU中添加了高速缓存(cache)为作为缓冲。
在执行任务时,CPU会先将运算所需要的数据copy到高速缓存中,让运算能够快速进行,当运算结束后,再将缓存中的结果刷回(flush back)主内存,这样CPU就不用等待主内存的读写操作了。
那么问题来了。每个处理器都有自己的高速缓存,同时又共同操作同一块内存,当多个处理器同时操作主内存时,可能导致数据不一致,这就是缓存一致性问题。
内存间交互操作
关于主内存和工作内存之间的交互协议说明:
内存交互操作有八种,虚拟机的实现保证每一个操作都是原子性的:
- lock(锁定):作用于主内存的变量,标识变量为线程独占状态
- unlock(解锁):作用于主内存的变量,释放一个处于锁定状态的变量,释放后的变量才可以被其他线程锁定
- read(读取):作用于主内存变量,从主内存中读取出后面load操作要用到的变量
- load(载入):作用于主内存中的变量,把刚才read的值放入工作内存的副本中
- use(使用):作用于工作内存中的变量,当线程执行某个字节码指令需要用到相应的变量时,把工作内存中的变量副本传给执行引擎
- assign(赋值):作用于工作内存中的变量,把一个从执行引擎中接受到的值放入工作内存的变量副本中
- store(存储):作用于工作内存中的变量,把工作内存中的变量送到主内存,给后续的write使用
- write(写入):作用于主内存中的变量,把store的工作内存中的变量值,写入主内存中
JMM对这八种指令的使用,制定了如下规则:
- read和load、store和write必须顺序执行,而且两个指令绑定出现;就是说出现read就要有load
- 不允许一个线程丢弃最近的assign操作,工作内存中的变量改变后,必须write同步到主内存
- 不允许一个线程把没有发生assign操作的变量同步到主内存
- 新的变量必须诞生于主内存,不允许工作内存使用一个没有初始化的变量;use、store操作变量之前,必须经过load和assign操作
- 变量同一时刻只允许一个线程对其lock,该线程可以对该变量加锁多次,释放锁需要执行相同次数的unlock,lock和unlock要成对出现
- 一个变量没有lock,不能unlock;并且一个线程不能unlock被其他线程锁住的变量
- 执行unlock前,必须把工作内存中的变量同步到主内存中
- 执行lock操作,需要清空工作内存(所有),并且需要使用该变量之前,要重新执行load和assign操作
对于volatile 型变量的特殊规则
对所有线程是立即可见的,对volatile变量所有的写操作都能够立即反映到其他线程之中。
换句话说,volatile变量在各个线程中是一致的。(正确)
基于以上论据,不能推出基于 volatile变量的运算在并发下是线程安全的。(需要注意)
原子性、可见性与有序性
见这篇介绍 链接:如何重新认识synchronized和volatile
先行发生(Happens-Before)原则
为什么有这个?
如果Java内存中的所有的有序性都靠 volatile 和synchronized,那么有很多操作都会变得啰嗦。
它是判断数据是否存在竞争,线程是否安全的非常有用的手段。通过这个原则,我们可以通过几条简单的规则一揽子解决并发环境下两个操作之间是否可能存在 冲突的所有问题。
下面是Java内存模型下一些“天然的”先行发生关系,这些先行发生关系无须任何同步器协助就已经存在,可以在编码中直接使用。 如果两个操作之间的关系不在此列,并且无法从下列规则推导出来的话,它们就没有顺序性保障,虚拟机可以对它们随意地进行重排序:
-
程序次序规则(Program Order Rule):在一个线程内,按照程序代码顺序,书写在前面的操作先行发生于书写在后面的操作。 准确地说,应该是控制流顺序而不是程序代码顺序,因为要考虑分支、 循环等结构。
-
管程锁定规则(Monitor Lock Rule):一个unlock操作先行发生于后面对同一个锁的lock操作。 这里必须强调的是同一个锁,而“后面”是指时间上的先后顺序。
-
volatile变量规则(Volatile Variable Rule):对一个volatile变量的写操作先行发生于后面对这个变量的读操作,这里的“后面”同样是指时间上的先后顺序。
-
线程启动规则(Thread Start Rule):Thread对象的
start()
方法先行发生于此线程的每一个动作。 -
线程终止规则(Thread Termination Rule):线程中的所有操作都先行发生于对此线程的终止检测,我们可以通过
Thread.join()
方法结束、Thread.isAlive()
的返回值等手段检测到线程已经终止执行。 -
线程中断规则(Thread Interruption Rule):对线程
interrupt()
方法的调用先行发生于被中断线程的代码检测到中断事件的发生,可以通过Thread.interrupted()
方法检测到是否有中断发生。 -
对象终结规则(Finalizer Rule):一个对象的初始化完成(构造函数执行结束)先行发生于它的
finalize()
方法的开始。 -
传递性(Transitivity):如果操作A先行发生于操作B,操作B先行发生于操作C,那就可以得出操作A先行发生于操作C的结论。
JVM内存区域的JMM的区别
JVM内存区域是指JVM运行时将内存数据分区域存储,强调对内存空间的划分。
JAVA内存模型是JAVA语言在多线程并发情况下对于共享变量内存操作的规范:解决变量在多线程的可见性、原子性的问题。
参考资料:
《深入理解Java虚拟机》周志明 著
以上是关于Java内存模型(JMM)的主要内容,如果未能解决你的问题,请参考以下文章