运行时数据区(JVM内存结构)
内存是非常重要的资源,是硬盘和CPU的中间桥梁,承载操作系统和应用程序的实时运行.JVM内存布局规定java在运行过程中内存申请、分配、管理的策略,保证JVM高效稳定运行。不同的JVM对于内存划分和管理机制存在部分差异(如J9和JRocket没有方法区,而Hotspot存在) 。
运行时数据区包括堆、方法区、PC寄存器(即程序计数器)、虚拟机栈、本地方法栈。
其中黄色的为多个线程共享,绿色的为单独线程私有的,即:
- 线程私有:PC寄存器(即程序计数器)、虚拟机栈、本地方法栈
- 线程共享:堆、方法区(永久区)
PC寄存器(程序计数器)
概述
JVM中程序计数寄存器用来存储下一条将要执行指令的地址(当前线程所执行的字节码的行号指示器),执行引擎从PC寄存器获取到指令地址后进行执行对应的指令。
PC寄存器特点:
- 内存空间很小几乎忽略不计
- 运行速度最快的内存区域
- 线程私有,与线程生命周期一致
- 存储当前线程正在执行的java方法的指令地址
- 不会出现OOM,无GC
常见问题
为什么要用PC寄存器记录当前线程的执行地址?使用它存储字节码指令地址有什么用?
因为CPU需要不停的切换各个线程,此时切换回来后,就得知道接着从哪里开始继续执行。
JVM的字节码解释器需要通过PC寄存器的值来明确下一条应该执行什么样的字节码指令。
PC寄存器为什么被设定为线程私有的?
程序通常运行在多线程环境下,CPU会不停的做任务切换,会导致程序经常中断和恢复,为了保证程序运行结果分毫不差,所以每个线程都有独立的PC寄存器,分别独立的记录各个线程正在执行的当前字节码指令地址,这样各个线程之间便可以独立计算,从而不会出现相互干扰,保证程序运行结果正确。
虚拟机栈
概述
由于跨平台设计,Java的指令都是根据栈来设计的,不同平台的CPU架构不同,所以不能设计为基于寄存器的。
栈是程序运行时基本单位(即解决程序如何运行、处理数据),堆是存储单位(即数据如何存储,存储在哪里)。
虚拟机栈特点:
- 跨平台
- 指令集小,编译器容易实现
- 性能相比寄存器下降(因为实现相同功能需要更多的指令)
- LIFO/FILO(后进先出/先进后出)
- 生命周期和线程周期保持一致
- JVM对栈操作只有两个:方法执行(入栈/压栈)和方法结束(出栈)
- 会出现OOM,无GC(因为只有进出栈)
- 可以通过-Xss设置栈内存大小
栈运行原理
- 不同线程中的栈帧是不允许存在相互引用的,即不可能在一个栈帧中引用另外一个线程的栈帧;
- 如果当前方法调用了其他方法,在方法返回时候,当前栈帧会将此方法的结果给到前一个栈帧,随后JVM会丢弃当前栈帧,使得前一个栈帧重新成为当前栈帧;
- 在java运行过程中正常的函数返回(return)和抛出异常都会导致栈帧被弹出.
栈帧
一个虚拟机栈内部保存多个栈帧,栈帧是虚拟机栈的基本组成单位,且每个栈帧对应java程序中的一个方法。栈帧是一个内存区块,即:
- 局部变量表(局部变量数组/本地变量表)
- 操作数栈(表达式栈)
- 动态链接(指向运行时常量池的方法引用)
- 方法返回地址(方法正常退出或者异常退出的定义)
- 其他附加信息
注意:有的资料中也将动态链接、方法返回地址、其他附加信息统一称为栈数据区
局部变量表
- 它被定义为一个数字数组,主要用于存储方法参数和定义在方法体内的局部变量。这些数据类型包括基本数据类型,对象引用以及方法返回地址类型
- 局部变量表所需的容量大小是在编译期确定下来的,并保存在方法的code属性的maximum local varaiables数据项中,在方法运行期间是不会改变局部变量表大小
- 属于线程私有数据,不存在数据安全问题
- 局部变量表中的变量只在当前方法调用中有效,方法调用结束后,随着方法栈帧销毁,局部变量表也随之销毁
- 局部变量表的存储基本单元被称为Slot(变量槽),32位的数据类型只占用一个slot,64位的类型占用两个slot
- 若当前帧由构造方法或者实例方法创建,那么该对象引用this将会存放在index为0的slot中
- 局部变量表中的slot是可以重用的,从而节省资源
- 局部变量必须显示赋值,否则编译不通过,而成员变量存在默认赋值,可不用显示赋值
- 局部变量表中的变量也是重要的垃圾回收根节点,主要被局部变量表直接或间接引用的对象都不会被回收
操作数栈(表达式栈)
操作数栈的作用就是在方法执行过程中,根据字节码指令,在栈中写入数据或者提取数据,即入栈(push)或者出栈(pop),比如执行求和,复制,交换等操作时。它的特点包括:
- 保存计算过程的中间结果,同时作为计算过程中变量的临时存储空间
- 操作数栈也会拥有一个栈深度用于存储数值。该深度也在编译期就定义好了,保存在方法的Code属性的max stack的值
- 操作数栈并非采用访问索引的方式来进行数据访问,而只能通过标准的入栈和出栈操作来完成一次数据访问
- 被调用方法有返回值时,该值也会被压入当前栈帧的操作数栈中
动态链接(指向运行时常量池的方法引用)
每个栈帧内部都包括一个指向运行时常量池中该栈帧所属方法的引用,该引用目的就是为了支持当前方法代码能够实现动态链接。
在Java源文件被编译到字节码文件中时,所有的变量和方法引用都作为符号引用保存在class文件的常量池里,而动态链接的作用就是为了将这些符号引用转换为调用方法的直接引用。
扩展
方法绑定机制:早期绑定和晚期绑定
静态链接:当一个字节码文件被装载进JVM时,如果被调用的目标方法在编译期可知,且运行期保持不变,这种情况下降调用方法的符号引用转换为直接引用的过程称之为静态链接
动态链接:如果被调用的目标方法在编译期无法被确定,只能通过运行期间将符号引用转换为直接引用,称为动态链接
绑定是一个字段、方法或者类在符号引用被替换为直接引用的过程,仅发生一次。而早期绑定可以对应静态链接,晚期绑定对应动态链接。
方法返回地址
方法返回地址就是用来存放调用该方法的pc寄存器的值。方法结束有正常退出和异常退出:
- 正常退出:调用者的pc寄存器的值作为返回地址,即调用该方法的指令的下一条指令地址
- 异常退出:返回地址通过异常表来确定,栈帧中一般不会保存这部分信息,且不会给他的上层调用者产生任何返回值
本地方法栈
本地方法: 一个Native方法就是一个Java调用非Java代码的接口,它的初衷就是融合C/C++程序;使用本地方法,我们可以用Java实现了Jre与底层系统的交互。
Java虚拟机用于管理Java方法的调用,而本地方法栈用于管理本地方法的调用。本地方法栈也是私有的。和Java虚拟机特点基本相同。
常见问题
开发中JVM的栈遇到的异常
首先Java的虚拟机规范允许Java的栈大小是动态的或者固定不变的。
a.当采用固定大小时,若线程请求分配的栈容量超过虚拟机栈允许的最大容器容量,则JVM会抛出StackOverFlowError异常
b.当采取动态扩展时,当尝试扩展时候无法申请到足够内存或者没有足够内存创建对应的虚拟机栈,则会抛出OutOfMemoryError异常
调整栈大小,保证不出现栈溢出吗?
不能,只能延迟栈溢出的时间
分配的栈内存越大越好吗?
不是,它避免不了栈溢出等异常,而且会占用其他内存空间
垃圾回收是否会涉及到虚拟机栈?
不会,它只存在栈溢出或者OOM,因为栈只涉及到入栈和出栈,但不会GC
方法中定义的局部变量是否线程安全?
得具体问题具体分析,
如果只有一个线程可以操作该变量,则线程安全;
如果多个线程可以操作该变量,则线程安全;比如通过形参传入一个stringbuilder非安全对象,若其他线程再操作stringbuilder对象时,则可能会改变结果,造成线程不安全;