java内存模型(JMM)
在介绍Java内存模型之前,先来看一下到底什么是计算机内存模型。
目标
学习计算机的主要组成
学习缓存的作用
计算机结构简介
冯诺依曼,提出计算机由五大组成部分,输入设备,输出设备,存储器,控制器,运算器。
CPU
中央处理器,是计算机的控制和运算的核心,我们的程序最终都会变成指令让CPU去执行,处理程序中
的数据。
内存
我们的程序都是在内存中运行的,内存会保存程序运行时的数据,供CPU处理。
缓存
CPU的运算速度和内存的访问速度相差比较大。这就导致CPU每次操作内存都要耗费很多等待时间。内
存的读写速度成为了计算机运行的瓶颈。于是就有了在CPU和主内存之间增加缓存的设计。最靠近CPU
的缓存称为L1,然后依次是 L2,L3和主内存,CPU缓存模型如图下图所示。
CPU Cache分成了三个级别: L1, L2, L3。级别越小越接近CPU,速度也更快,同时也代表着容量越
小。
- L1是最接近CPU的,它容量最小,例如32K,速度最快,每个核上都有一个L1 Cache。
- L2 Cache 更大一些,例如256K,速度要慢一些,一般情况下每个核上都有一个独立的L2 Cache。
- L3 Cache是三级缓存中最大的一级,例如12MB,同时也是缓存中最慢的一级,在同一个CPU插槽
之间的核共享一个L3 Cache。
Cache的出现是为了解决CPU直接访问内存效率低下问题的,程序在运行的过程中,CPU接收到指令
后,它会最先向CPU中的一级缓存(L1 Cache)去寻找相关的数据,如果命中缓存,CPU进行计算时就
可以直接对CPU Cache中的数据进行读取和写人,当运算结束之后,再将CPUCache中的最新数据刷新
到主内存当中,CPU通过直接访问Cache的方式替代直接访问主存的方式极大地提高了CPU 的吞吐能
力。但是由于一级缓存(L1 Cache)容量较小,所以不可能每次都命中。这时CPU会继续向下一级的二
级缓存(L2 Cache)寻找,同样的道理,当所需要的数据在二级缓存中也没有的话,会继续转向L3
Cache、内存(主存)和硬盘。
小结
计算机的主要组成CPU,内存,输入设备,输出设备。
Java内存模型
程序计数器(线程私有)
也称PC 寄存器。保存的是程序当前执行的指令的地址(也可以说保存下一条指令的所在存储单元的地址),当 CPU 需要执行指令时,需要从程序计数器中得到当前需要执行的指令所在存储单元的地址,然后根据得到的地址获取到指令,在得到指令之后,程序计数器便自动加 1 或者根据转移指针得到下一条指令的地址,如此循环,直至执行完所有的指令。也就是说是用来指示执行哪条指令的。
由于在 JVM 中,多线程是通过线程轮流切换来获得 CPU 执行时间的,因此,在任一具体时刻,一个 CPU 的内核只会执行一条线程中的指令,因此,为了能够使得每个线程都在线程切换后能够恢复在切换之前的程序执行位置,每个线程都需要有自己独立的程序计数器,并且不能互相被干扰,否则就会影响到程序的正常执行次序。
因此,可以这么说,程序计数器是每个线程所私有的。
在 JVM 规范中规定,如果线程执行的是非 native 方法,则程序计数器中保存的是当前需要执行的指令的地址
;如果线程执行的是 native 方法,则程序计数器中的值是 undefined
。
由于程序计数器中存储的数据所占空间的大小不会随程序的执行而发生改变
,因此,对于程序计数器是不会发生内存溢出(OutOfMemory)的。
异常情况:不存在
2.虚拟机栈(线程私有)
Java 栈中存放的是一个个的栈帧,每个栈帧对应一个被调用的方法,在栈帧中包括局部变量表、操作数栈、指向当前方法所属的类的运行时常量池的引用、方法返回地址、额外的附加信息。
当线程执行一个方法时,就会随之创建一个对应的栈帧,并将建立的栈帧压栈
。当方法执行完毕之后,便会将栈帧出栈。
因此可知,线程当前执行的方法所对应的栈帧
必定位于 Java 栈的顶部
1、局部变量表,用来存储方法中的局部变量(包括在方法中声明的非静态变量以及函数形参)。。
- 对于基本数据类型的变量,则直接存储它的值,
- 对于引用类型的变量,则存的是指向对象的引用。
局部变量表的大小在编译器就可以确定其大小了,因此在程序执行期间局部变量表的大小是不会改变的。存储内容:引用对象,returnAddress 类型
。Long 和 double 类型占用 2 个局部变量空间,其余的数据类型占据一个。
局部变量表空间在编译期间完成分配。
2、操作数栈
栈最典型的一个应用就是用来对表达式求值。想想一个线程执行方法的过程中,实际上就是不断执行语句的过程,而归根到底就是进行计算的过程。因此可以这么说,程序中的所有计算过程都是在借助于操作数栈来完成的。
3、指向运行时常量池的引用,因为在方法执行的过程中有可能需要用到类中的常量,所以
必须要有一个引用指向运行时常量。
4、方法返回地址,当一个方法执行完毕之后,要返回之前调用它的地方(参考汇编),因
此在栈帧中必须保存一个方法返回地址。
由于每个线程正在执行的方法可能不同,因此每个线程都会有一个自己的 Java 栈线程
异常情况:
- 栈深度大于已有深度:
StackOverflowError
- 可扩展深度大于能够申请的内存:
OutOfMemoryError
通过 -Xss 这个虚拟机参数来指定每个线程的 Java 虚拟机栈内存大小,在 JDK 1.4 中默认为 256K,而在 JDK 1.5+ 默认为 1M: java -Xss2M HackTheJava
3.本地方法栈(线程私有)
本地方法栈与 Java 栈的作用和原理非常相似。
区别只不过是 Java 栈是为执行 Java方法服务的,而本地方法栈则是为执行本地方法(Native Method)服务的。
在 JVM 规范中,并没有对本地方发展的具体实现方法以及数据结构
作强制规定,虚拟机可以自由实现它。在 HotSopt 虚拟机中直接就把本地方法栈和 Java 栈合二为一。
本地方法栈是用其他语言c, c++或汇编语言编写的,并且被便以为基于本机硬件的操作系统的程序。
异常情况:同虚拟机栈;
4.堆(线程共享)
Java中的堆是用来存储对象本身以及数组(当然,数组引用是存放在虚拟栈)
堆是被所有线程共享的,在 JVM 中只有一个堆
垃圾收集器管理的主要区域,很多时候被称作 GC 堆。
新生代和老年代,再细致点 Eden 空间,From Survivor 空间,ToSurvivor 空间等。 urvivor 英[səˈvaɪvə®]
现代的垃圾收集器基本都是采用分代收集算法,其主要的思想是针对不同类型的对象采取不同的垃圾回收算法(复制算法,标记整理)。可以将堆分成两块:
- 新生代(Young Generation)新生代1/3
- 老年代(Old Generation)( eden:survivor1: survivor2 => 8:1:1)老年代2/3)
堆不需要连续内存,并且可以动态增加其内存,增加失败会抛出 OutOfMemoryError
异常。
可以通过 -Xms 和 -Xmx 这两个虚拟机参数来指定一个程序的堆内存大小,第一个参数设置初始值,第二个参数设置最大值。
异常情况:
物理上不连续的内存空间,逻辑连续即可。既可实现固定大小,也可扩展。
如果堆中没有内存完成实例分配,并且堆无法再扩展是,将会抛出 OutOfMemoryError;
补充
栈上分配:几乎所有的对象实例,都是在堆上分配的,但存在部分例外,栈上分配就是这种除了堆上分配的例外。
1.栈上分配指的是什么?
①将线程中的私有对象打散(即代码中user),让它在栈上分配,而不是在堆上分配
public static void main(String[] args) {
User user = new User();
}
比如方法中的user引用,就是方法的局部变量,new User()实例在堆上,我们需要的就是把这个实例打散:比如我们的实例user中有两个字段,就把这个实例认作它内部的两个字段以局部变量的形式分配在栈上也就是打散,这个操作称为:标量替换
②使用栈上分配策略除了需要开启标量替换,还需要开启逃逸分析。
什么是逃逸分析?
其实就是判断我们将这个user对象会不会return出去,出去了的话,这时候我们对于这个对象来说就不会受用栈上分配,因为后续的代码可能还需要使用这个对象实例,可以说只要是多个线程共享的对象就是逃逸对象
5.方法区(线程共享)
方法区中,存储了每个类的信息(包括类的名称、方法信息、字段信息)、静态变
量、常量以及编译器编译后的代码等。
在 Class 文件中除了类的字段、方法、接口等描述信息外,还有一项信息是常量池,用来存储编译期间生成的字面量和符号引用。
方法区还有一块内存,运行时常量池,它是每一个类或接口的常量池的运行时表示形式,在类和接口被加载到 JVM 后,对应的运行时常量池就被创建出来。当然并非 Class文件常量池中的内容才能进入运行时常量池,在运行期间也可将新的常量放入运行时常量池中,比如 String 的 intern 方法。
在 JVM 规范中,没有强制要求方法区必须实现垃圾回收。很多人习惯将方法区称为“永久代”,是因为 HotSpot 虚拟机以永久代来实现方法区,从而 JVM 的垃圾收集器可以像管理堆区一样管理这部分区域,从而不需要专门为这部分设计垃圾回收机制。
不过自从 JDK7之后,Hotspot 虚拟机便将运行时常量池从永久代移除了。
异常情况:
- 方法区调用递归,内存会溢出,报 OutOfMemoryError;
- 当常量池无法再申请到内存时 OutOfMemoryError;
从 JDK 1.8 开始,移除永久代,并把方法区移至元空间,它位于本地内存中,而不是虚拟机内存中。
HotSpot 虚拟机把它当成永久代来进行垃圾回收。但很难确定永久代的大小,因为它受到很多因素影响,并且每次 Full GC 之后永久代的大小都会改变,所以经常会抛出 OutOfMemoryError 异常。
方法区是一个 JVM 规范,永久代与元空间都是其一种实现方式。
在 JDK 1.8 之后,原来永久代的数据被分到了堆和元空间中,元空间存储:类的元信息,静态变量和常量池等放入堆中。