java内存模型与生命周期以及JVM
java内存模型与线程
JMM
-
Java内存模型是一种规范:
Java虚拟机规范中定义了Java内存模型(Java Memory Model,JMM),用于屏蔽掉各种硬件和操作系统的内存访问差异,以实现让Java程序在各种平台下都能达到一致的并发效果; -
JMM规范了Java虚拟机与计算机内存是如何协同工作的:
规定了一个线程如何和何时可以看到由其他线程修改过后的共享变量的值,以及在必须时如何同步的访问共享变量。
-
Java内存模型(不仅仅是放在JVM内存分区):
调用栈和本地变量存放在线程栈上, 对象存放在堆上;
在JVM内部,Java内存模型把内存分成了两部分:线程栈区和堆区,下图展示了Java内存模型在JVM中的逻辑视图;
-
基本类型的本第变量, 总是存放于线程栈上;
-
一个本地变量也可能是指某个对象的一个引用, 引用(该变量)存放在线程栈上, 但对象本身存放在堆上;
-
一个对象可能包含方法, 这些方法可能包含本地变量; 这些本地变量仍然存放在线程栈上, 其所属的对象存放在堆上;
-
对象的成员变量随这个对象自身存放于堆堆上;(无论其为基本类型还是引用类型)
-
静态成员变量随堆定义一起存放在堆上;
-
存放在堆上 的对象可以被所持有对这个对象引用的线程访问;
当一个线程访问对象时, 它也可以访问其成员变量;
若两个线程同时调用一个对象上的同一方法, 他们都会访问这个对象的成员变量, 但每个线程都拥有这个成员变量的私有拷贝;
-
硬件内存架构
现代硬件内存模型与java内存模型有些不同, 简单图示如下:
-
多CPU
现代计算机通常有两个或者多个CPU, 其中一些CPU还有多核; 每个CPU能在某一时刻运行一个线程, 若Java程序为多线程, 每个CPU上的一个线程可能同时(并发)执行;
-
CPU寄存器
是CPU内内存的基础, 执行操作的速度远大于主存;(CPU访问寄存器的速度更快)
-
高速缓存cache
由于计算机的存储设备和CPU的运算速度有几个数量级的差距, 所以加入读写速度接近CPU的cache作为CPU和主存之间的缓冲:
将运算所需要的数据赋值到cache中, 快速进行运算, 运算结束后再从cache同步到内存中, 这样CPU就无需等待内存读写;
-
缓存一致性: 当多个处理器的运算任务都涉及同一块主内存区域时,将可能导致各自的缓存数据不一致的情况所以,需要各个处理器访问缓存时都遵循一些协议,在读写时要根据协议来进行操作;
-
-
内存
一个计算机包含多个主存, 所有CPU都可以访问主存; 主存通常远大于缓存;
-
运作原理
cpu需要读取主存时, 先将主存的部分读入缓存, 将缓存的部分读入内部寄存器, 然后在寄存器中执行操作; cpu要将结果写入主存时, 它会将内部寄存器的值同步至缓存, 然后再某个时间同步至主存;
JMM和硬件架构直接的桥接
java内存模型与硬件内存架构之间存在差异, 硬件内存架构没有区分线程栈和堆;
对于硬件, 所有线程栈和堆都分布于主存中, 部分线程可能出现在缓存和寄存器中, 如下图所示:
从抽象角度, JMM定义了线程和主存之间的抽象关系
-
线程之间的共享变量存储在主存中;
-
每个线程都有一个私有本地内存, 本地内存是JMM的一个抽象概念, 并不真实存在; 它涵盖了缓存\写缓存区\寄存器及其他的硬件和编译器优化; 本地内存中存储了该线程以读\写共享变量的拷贝副本;
-
从更低层次来讲, 主存就是硬件的内存, 而为了获得更好的运行速度, 虚拟机及硬件系统会让工作内存有限存储于寄存器和高速缓存中;
-
Java内存模型中的线程的工作内存(working memory)是cpu的寄存器和高速缓存的抽象描述; 而JVM的静态内存存储模型(JVM内存模型)总是一种会内存的物理划分, 它只局限于JVMd\内存;
JMM模型下的线程通信
线程通信必须经过主存
如下, 线程A与线程B之间的通信必须经过以下两个步骤:
-
线程A把本地内存A中更新过的变量刷新至主内存;
-
线程B到主内存中读取线程A更新过的共享变量;
关于主内存到工作内存之间的具体交互协议, 即一个变量如何从主内存拷贝到工作内存\如何从工作内存同步到主内存之间的实现细节, java内存模型定义了以下八种操作来完成:
lock | 锁定 | 用于主内存变量 | 将变量表示为某一线程独占状态 |
---|---|---|---|
unlock | 解锁 | 用于主内存变量 | 释放锁定变量, 使可被其他线程锁定 |
read | 读取 | 用于主内存变量 | 把变量值输送到工作内存, 以便load动作使用 |
load | 载入 | 用于工作内存变量 | 把read操作得到的变量值放入工作区的变量副本中 |
use | 使用 | 用于工作内存变量 | 把一个变量值传递给执行引擎, 每当虚拟机遇到一个给变量赋值的字节码指令时执行这个操作(从工作内存中读取数据来计算) |
assign | 赋值 | 用于工作内存变量 | 把一个从执行引擎接收到的值赋值给工作内存的变量,每当虚拟机遇到一个给变量赋值的字节码指令时执行这个操作(将计算好的值重新赋值到工作内存中) |
store | 存储 | 用于工作内存变量 | 把工作内存中的一个变量的值传送到主内存中,以便随后的write的操作 |
write | 写入 | 用于主内存变量 | 作用于主内存的变量,它把store操作从工作内存中一个变量的值传送到主内存的变量中 |
示例:
JVM
JVM是Java Virtual Machine(Java虚拟机)的缩写,JVM是一种用于计算设备的规范,它是一个虚构出来的计算机,是通过在实际的计算机上仿真模拟各种计算机功能来实现的。
引入Java语言虚拟机后,Java语言在不同平台上运行时不需要重新编译。Java语言使用Java虚拟机屏蔽了与具体平台相关的信息,使得Java语言编译程序只需生成在Java虚拟机上运行的目标代码(字节码),就可以在多种平台上不加修改地运行。
- JVM就是java虚拟机, 是用来执行java字节码(二进制)的虚拟计算机;
- JVM运行在操作系统之上, 与硬件没有关系;
JVM跨平台原理
JVM的分类
- 类加载子系统
- 运行时数据区(重点关注栈\堆\方法区)
- 执行引擎(JIT编译器和解释器共存)
- JIT编译器(主要影响性能): 编译执行; 一般热点数据会进行二次编译, 将字节码指令变成机器指令, 将机器指令放在方法区缓存;
- 解释器(负责响应时间,响应时间很快): 逐行解释字节码;
JVM的位置
JVM运行于操作系统之上, 应用程序之下,与硬件没有直接交互
JVM的体系结构
- 入口是编译好的字节码文件(编译器前端), 经过类加载子系统(将字节码加载到内存中, 生成class对象: 加载–>链接–>初始化)
- 内存中, 多个对象共享方法区和堆区;(多个线程共享)
- Java虚拟机栈\本地方法栈\程序计数器 每个线程独享一份;
- 执行引擎: 解释器(解释执行)\JIT及时编译器(编译器后端)\垃圾回收器 三部分构成
每个区域的详细作用:
-
java堆(heap)
JVM所管理的内存中最大的一块, 被所有线程共享, 在虚拟机启动时创建;
堆的唯一目的就是存放对象实例, 几乎所有对象实例都在这里分配内存;
Java堆是垃圾收集器管理的主要区域, 也被成为 GC堆 ;
根据JVM规定, Java堆可以是逻辑上连续, 物理上处于不连续的内存空间;
如果堆中没有完成实例分配, 且无法扩展时, 会抛出OutOfMemoryError;
-
方法区(method area)
时各个线程共享的内存, 用于存储已被虚拟机加载的类的信息\常量\静态变量\即时编译器编译后的代码等数据;
和Java堆一样不需要连续的内存和可以选择固定大小或可扩展外, 可以选择不实现垃圾收集;这个区域的内存回收目标主要针对常量池的回收和对类的卸载;
当方法区无法满足内存分配需求时,将抛出OutOfMemoryError;
-
JVM栈(JVM stacks)
线程私有, 生命周期与线程相同;
JVM栈式描述Java方法执行的内存模型: 每个方法被执行的时候都会同时创建一个栈帧(stack frame)用于存放局部变量表\操作栈\动态链接\方法出口等信息; 每个方法被调用直到执行完成的过程, 就对应着一个栈帧在虚拟机中从入栈到出战的过程;
局部变量表存放了编译期可知的基本数据类型\对象引用和returnAddress类型;
其中64位长度的long和double类型的数据会占用2个局部变量空间(slot), 其余的数据类型只占用1个; 在方法运行期间不会改变局部变量表的大小;
若线程请求的栈深度, 将抛出StackOverflowError; 若JVM栈可以动态扩展, 当扩展时无法申请到足够的内存时会抛出OutOfMemoryError;
-
本地方法栈(native method stacks)
JVM栈为虚拟机执行Java方法(字节码)服务, 本地方法栈则是为虚拟机使用到的native方法服务;
虚拟即规范堆本地方法栈中的方法使用的语言\使用方法\数据结构并没有强制规定, 因此具体的虚拟机可以自由实现;
本地方法也会抛出StackOverflowError及OutOfMemoryError;
-
程序计数器(program counter register)
一块较小的内存空间, 作用是当前线程所执行的字节码的行号指示器; 字节码指示器通过改变这个计数器的值来选取下一条需要执行的字节码指令; 分支\循环\跳转\异常处理\线程恢复等基础功能都要依赖这个计数器来完成;
每条线程都有一个独立的计数器, 各线程之间的计数器互不影响, 独立存储, 我们称这类内存区域为"线程私有"的内存;
若线程执行的时java方法, 计数器记录的是正在执行的虚拟机字节码指令的地址;若正在执行的是native方法, 计数器值则为空(Undefined);
计数器是唯一一个在JVM规范中没有规定任何OutOfMemoryError情况的区域;
示例:
import java.text.SimpleDateFormat;
import java.util.Date;
import org.apache.log4j.Logger;
public class HelloWorld {
private static Logger LOGGER=Logger.getLogger(HelloWorld.class.getName());
public void sayHello(String message) {
SimpleDateFormat formatter = new SimpleDateFormat("dd.MM.YYYY");
String today = formatter.format(new Date());
LOGGER.info(today + ": " + message);
}
}
Java代码的执行流程
JVM的生命周期
-
虚拟机的启动
通过引导类加载器bootstrap class loader创建初始类来完成, 这个类由虚拟机具体实现指定;
-
虚拟机的执行
- 运行中的java虚拟机有明确任务, 执行java程序;
- 程序开始虚拟机同时开始运行, 程序结束虚拟机也结束;
- 执行java程序时, 真正在执行的时一个java虚拟机进程;
-
虚拟机的停止
以下几种情况会导致退出虚拟机:
- 程序正常执行结束;
- 程序运行过程中遇到错误或异常而终止执行;
- 操作系统故障导致虚拟机进程终止;
- 线程调用了Runtime类或System类的exit()方法, 或调用了Runtime类的halt()方法, 且Java安全管理器允许执行安全退出方法;