内存区域、内存模型
- 内存区域:即运行时数据区域,指JVM对于不同类型数据在内存中的存储方式
- 内存模型(JMM:Java Memory Model):定义了线程与主内存之间的抽象关系,即JVM在内存中的工作方式,即JVM使用内存区域中的数据的方式
JDK8之后的内存区域:
* Native Method Stacks(本地方法栈)
* Program Counter Register(程序计数器)
* Java Virtual Machine Stacks(JVM Stacks,即虚拟机栈)* Stack Frame(栈帧)* 局部变量表* 操作栈* 动态连接* 方法返回地址* Stack Frame(栈帧)* 局部变量表* 操作栈* 动态连接* 方法返回地址
* Heap(堆区)* Young区(新生代)* Eden* S0* S1* Old区(老年代)
* Method Area(方法区)* Runtime Constant Pool(运行时常量池)* 方法元信息* klass类元信息
* CodeCache(JIT编译产物)
Program Counter Register (程序计数器)
- 作用:当前线程所执行的字节码的行号指示器,当多线程切换时,使线程恢复后找到正确的执行位置
- 特点:
- 内存占用少
- 线程私有
- 当前线程执行Java方法,计数器保存虚拟机中字节码指令地址;执行Native方法,记录null
- 唯一一个在JVM规范中没有规定OutOfMemoryError的区域
JVM Stacks
- 概念:
- 活动线程:当前正在执行的线程
- 当前帧:正在执行的方法对应的栈帧,由于只有栈顶帧有效,所以当前帧也是栈顶帧
- 当前方法:正在执行的方法
- 简介:描述了Java方法执行的内存模型,每个方法在执行时都会创建一个栈帧(Stack Frame:栈中的一个元素,方法运行时的基础数据结构),存储局部变量表,操作数栈、动态连接、方法出口等。每一个方法从调用到执行完的过程,对应一个栈帧入栈出栈过程
- 作用:保存Java方法执行的时候需要的各种数据、参数
- 特点:
- 线程私有
- 生命周期与线程相同
- 执行引擎运行时,所有指令都只能对当前帧操作
- 一个方法对应一个栈帧
- 组成:
-
Stack Frame(栈帧):
- 介绍:一个栈帧对应一个方法执行需要的数据
- JVM规定的与栈帧相关异常:
- 线程请求的栈深度大于JVM允许的深度,抛出StackOverflowError
- 对于当前大多数可以拓展的JVM栈,在拓展时申请不到足够内存,抛出OutOfMemoryError
- 组成:
-
局部变量表
- 作用:存放方法参数和局部变量,字节码指令中的STORE指令就是将操作栈中计算完成局部变量写到当前帧的局部变量表中
- 特点:
- 必须显示初始化,才能使用
- 如果是非静态方法,会在index[0]位置存储方法所属对象的实例引用,占用4字节,后面存储方法参数和局部变量
-
操作栈
- 作用:用于方法执行过程中信息的存取
- 简介:JVM的执行引擎是基于栈的执行引擎,其中的栈即操作栈,方法执行时,使用操作栈进行存取信息
- 关联:
- 字节码指令集即基于栈类型,栈深度保存在方法元信息的栈属性中
- 例:
- ++i:总体思路是按顺序执行,先将局部变量表中的i执行加1操作(load memory&add&store memory),然后放到操作栈中(load memory),此时从操作栈栈顶取出的i就是加1之后的
- i++:按照按顺序执行的思路,先将i放到操作栈中(load memory),然后执行加1的操作(add memory),加1之后的值更新到局部变量表中(store memory),此时从栈顶读取到的i未加1,所以如果多线程操作i的时候,可能从局部变量表读到的i比预期的小
- 特点:
- 初始状态为空桶式结构栈
-
动态连接
- 介绍:每个栈帧中包含一个在常量池中对当前方法的引用,目的是为了支持方法调用过程的动态连接
-
方法返回地址:
- 方法执行退出情况:
- 正常退出,遇到返回字节码指令,比如return、ireturn、areturn
- 异常退出
- 介绍:即方法调用完成返回的位置,方法调用完成后会弹出当前栈帧
- 退出后行为:
- 返回值压入上层调用栈帧
- 异常抛给能处理的栈帧
- 程序计数器指向方法调用后的下一条指令
- 方法执行退出情况:
-
-
Native Method Stacks(本地方法栈)
- 介绍:与JVM Stacks类似,但保存的是Native方法对应的栈帧
- 特点:
- 也会抛出StackOverFlowError和OutOfMemoryError
- 线程调用本地方法时,不受JVM约束
- 本地方法通过JNI(Java Native Interface)来访问JVM的数据,可以调用寄存器,具有JVM相同的能力、权限
- 大量本地方法运行时,会削弱JVM对系统控制力,本地方法出错信息比较黑盒
- 本地方法栈会在内存不足时抛出NativeHeapOutOfMemory
Heap(堆)
- 介绍:存放对象实例,几乎所有的对象实例都在这里分配内存
- 特点:
- 对大多数应用来说,Heap是JVM管理内存中最大的一块
- 被所有线程共享
- 虚拟机启动时创建
- GC管理的主要区域,有时候也成Heap为GC Heap
- 内部可能包含多个线程私有的分配缓冲区(Thread Local Allocation Buffer,TLAB)
- 可以由物理上不连续的内存空间组成,只要逻辑上连续即可
- 当堆中无内存完成实例分配,并且无法拓展时,会抛出OutOfMemoryError
- 按照GC中的分代回收算法分类:
- 新生代
- Eden
- S0
- S1
- 老年代
- 新生代
Metaspace(元数据区)
- 组成:
直接内存
- 作用:
- 用于避免Java堆与native对来回复制数据,提高某些场景的性能
- 特点:
- 非虚拟机运行时数据区的一部分,也不是虚拟机规范中定义的内存区域
- 基于通道(Channel)与缓冲区(Buffer)的I/O方式,可以使用Native函数库直接分配堆外内存,然后通过一个存储在java堆中的DirectByteBuffer对象作为这块内存的引用来操作
- 动态拓展时申请不到足够内存会抛出OutOfMemoryError
Java Memory Model(java内存模型)
- 作用:
- 控制线程之间的通信,决定一个线程对共享变量的写入何时对另一个线程可见
- 定义程序中各个变量的访问规则
- 特点:
- 共享内存的并发模型,线程之间通过读写共享变量(堆内存中的实例域、静态域、数组元素)来完成隐式通信
- 所有变量都存储在主内存中,每条线程有自己的私有内存,也叫工作内存,线程对变量的操作必须在工作内存中进行,不能直接操作主内存中的变量,工作内存中存储了变量副本
- 属于语言级的内存模型,在不同的编译器、处理器上,通过禁止特定类型的编译器重排序和处理器重排序(指令级并行重排序、内存系统重排序),确保为程序员提供一致的内存可见性
重排序
- 作用:
- 编译器和处理器为了提高性能,会对指令重排序
- 分类:
- 编译器优化的重排序:
- 编译器在不改变程序语义的情况下,可以重排语句执行顺序
- 指令级并行的重排序:
- 现代处理器提供了指令级并行技术来将多条指令并行执行,如果不存在数据依赖性,处理器可以改变语句对应机器指令的执行顺序
- 内存系统的重排序:
- 由于处理器使用了缓存和读写缓冲区,使加载和存储操作看上去是在乱序执行
- 编译器优化的重排序:
- 从Java源代码到执行指令序列:
- 源代码 > 编译器优化重排序 > 指令级重排序 > 内存系统重排序 > 最终执行的指令序列
- Java进制处理器重排序方式:
- 生成指令序列的内存屏障,即重排序时不能把内存屏障指令重排到之前来实现的
happens-before
- 作用:
- 描述操作之间的内存可见性(可见性:指当一条线程修改了这个变量的值,新值对于其他线程来说是可以立即得知的)
- 特点:
- JDK5出现
- 如果一个操作执行的结果需要对另一个操作可见,那么这两个操作之间必须存在 happens-before 关系。这里提到的两个操作既可以是在一个线程之内,也可以是在不同线程之间
- 重要的 happens-before 规则如下:
- 程序顺序规则:一个线程中的每个操作,happens- before 于该线程中的任意后续操作。
- 监视器锁规则:对一个监视器锁的解锁,happens- before 于随后对这个监视器锁的加锁。
- volatile 变量规则:对一个 volatile 域的写,happens- before 于任意后续对这个 volatile 域的读。
- 传递性:如果 A happens- before B,且 B happens- before C,那么 A happens- before C。
volatile
- 作用:
- 保持变量的修改在多个线程间是同步的
- 特点:
- 保证变量对所有线程都是可见的
- volatile变量在并发情况下由于Java的非原子化操作导致线程不安全,synchronized由于统一时间内只能由一个线程操作,所以是线程安全的
- 禁止指令重排序优化