一、Java虚拟机栈
- 每个线程在创建时都会创建一个虚拟机栈,其内部保存一个个的栈帧,每一个栈帧对于一个方法
- Java虚拟机栈是线程私有的
- Java虚拟机栈的生命周期和线程一致
- 虚拟机栈主要管理程序的运行,保存方法的局部变量、部分结果,并参与方法的调用和返回
二、栈的优势
- 栈是一种快速有效的分配存储方式,访问速度仅次于程序计数器
- JVM操作栈,当方法执行时,执行入栈操作,当方法结束时,执行出栈操作
- 栈不存在垃圾回收问题,但是存在栈溢出问题
三、栈的存储单位
- 每个线程都有自己的栈,栈中的数据都是以栈帧为基本单位进行存储的
- 在这个线程上正在执行的每个方法都各自对应一个栈帧
- 栈帧是一个内存区块,是一个数据集,维系着方法执行过程中的各种数据信息
四、栈的运行原理
- JVM操作栈只有入栈和出栈
- 在一条活动线程中,一个时间点上,只会有一个活动的栈帧,即栈顶栈帧,称为当前栈帧。与当前栈帧相对应的方法就是当前方法,定义这个方法的类就是当前类
- 执行引擎运行的所有字节码指令只针对当前栈帧进行操作
- 如果在该方法中调用了其它方法,对于的新的栈帧会被创建出来,放在栈的顶端,成为新的当前栈帧
- 不同线程中所包含的栈帧是不允许相互引用的,即不可能在另一个栈帧中引用另外一个线程的栈帧
- 如果当前方法调用了其他方法,方法返回之际,当前栈帧会传回此方法的执行结果给前一个栈帧,接着,虚拟机会丢弃当前栈帧,使得前一个栈帧重新成为当前栈帧
- Java方法有两种返回函数的方式,一种是正常的函数返回,使用
return
指令;另外一种是抛出异常。不管使用哪种方式,都会导致栈帧被弹出。
五、栈帧的内部结构
- 局部变量表
- 操作数栈
- 动态链接(指向运行时常量池的方法引用)
- 方法返回地址(方法正常退出或者异常退出的定义)
- 一些附加信息
局部变量表
- 定义为一个数字数组,主要用于存储方法参数和定义在方法体内的局部变量,这些数据类型包括各类基本数据类型、对象引用、返回值类型
- 由于局部变量表示建立在线程的栈上,是线程的私有数据,也就不会有多线程之间的安全问题
- 局部变量表所需的容量大小实在编译期确定下来的
- 方法嵌套调用的次数由栈的大小决定,方法又对应着栈帧,栈帧的大小是由局部变量表和操作数栈决定的
- 局部变量表中的变量只在当前方法调用中有效,在方法执行时,虚拟机通过使用局部变量表完成参数值到参数变量列表的传递过程。当方法调用结束后,随着方法栈帧的销毁,局部变量表也会随着销毁
变量槽Slot
- 局部变量表最基本的存储单元是
Slot
(变量槽) - 在局部变量表中,32位以内的类型只占用一个
slot
,64位的类型(long和double)占用两个slot
- JVM会为局部变量表中的每一个
Slot
都分配一个访问索引,通过这个索引即可访问到局部变量表中指定的局部变量值 - 当一个实例方法被调用的时候,它的方法参数和方法体内部定义的局部变量将会按照顺序被复制到局部变量表中的每一个
Slot
上 - 如果当前帧是由构造方法或者实例方法创建的,那么该对象引用
this
将会存放在index
为0
的slot
处,其余的参数按照参数表顺序继续排列
静态变量与局部变量的对比
- 静态变量有两次初始化的操作,第一次是在链接阶段的准备阶段,进行默认的赋值操作,第二次是在初始化阶段,进行显示赋值操作
- 局部变量表不存在系统初始化的过程,所以只要定义了局部变量就必须进行显示赋值操作,否则编译不通过
操作数栈
- 操作数栈,在方法执行过程中,根据字节码指令,往栈中写入数据或提取数据,即入栈和出栈
- 操作数栈,主要用于保存计算过程的中间结果,同时作为计算过程中变量临时的存储空间。
- 操作数栈就是JVM执行引擎的一个工作区,当一个方法开始执行的时候,一个新的栈帧也会随之被创建出来,这个方法的操作数栈是空的
- 每一个操作数栈都会拥有一个明确的栈深度用于存储数值,其所需的最大深度在编译期就定义好了,保存在方法的
code
属性中,为max_stack
的值 - 栈中的任何一个元素都是可以任意的java数据类型
32bit
的类型占用一个栈单位深度,64bit
的类型占用两个栈深度单位 - 操作数栈并非采用访问索引的方式来进行数据访问的,而是只能通过入栈
push
和出栈pop
操作来完成一次数据访问 - 如果被调用的方法带有返回值的话,其返回值将会被压入当前栈帧的操作数栈中,并更新PC寄存器中下一条需要执行的字节码指令
- 操作数栈中的元素的数据类型必须与字节码指令的序列严格匹配,这由编译器在编译期间进行验证,同时在类加载过程中的类验证阶段的数据流分析阶段要再次验证
- 另外,我们说Java虚拟机的解释引擎是基于栈的执行引擎,其中的栈指的就是操作数栈
方法(栈帧)的执行过程:
15
压入操作数栈
- 将操作数
15
出栈,存储到局部变量表索引为1
的位置,0
存放在this
8
压入操作数栈
- 将操作数
8
出栈,存储到局部变量表索引为2
的位置
- 从局部变量表中把索引为
1
和2
的是数据取出来,放到操作数栈
- 操作数
8
和15
出栈,执行相加操作,并将计算结果压入操作数栈中
- 计算结果
23
出栈,存储在局部变量表索引为3
的位置上
动态链接
- 每一个栈帧内部都包含一个指向运行时常量池中该栈帧所属方法的引用。包含这个引用的目的就是为了支持当前方法的代码能够实现动态链接
- 在Java源文件被编译成字节码文件中时,所有的变量和方法引用都作为符号引用保存在
class
文件的常量池里。比如描述一个方法调用了另外的其他方法时,就是通过常量池中指向方法的符号引用来表示的,那么动态链接的作用就是为了将这些符号引用转换为调用方法的直接引用
在JVM中,将符号引用转换为调用方法的直接引用,与方法的绑定机制相关
-
当一个 字节码文件被装载进JVM内部时,如果被调用的目标方法在编译期可知,且运行期保持不变时,这种情况下将调用方法的符号引用转换为直接引用的过程称之为静态链接
-
如果被调用的方法在编译期无法被确定下来,也就是说,只能够在程序运行期间将调用方法的符号引用转换为直接引用,由于这种引用转换过程具备动态性,因此也就被称之为动态链接
-
对应的方法的绑定机制为早期绑定 和晚期绑定 。绑定是一个字段、方法或者类在符号引用被替换为直接引用的过程,这仅仅发生一次
-
早期绑定就是指被调用的目标方法如果在编译期可知,且运行期保持不变时,即可将这个方法与所属的类型进行绑定,这样一来,由于明确了被调用的目标方法究竟是哪一个,因此也就可以使用静态链接的方式将符号引用转换为直接引用
-
如果被调用的方法在编译期无法被确定下来,只能够在程序运行期间根据实际的类型绑定相关的方法,这种绑定方式也就被称之为晚期绑定。
方法返回地址
- 存放调用该方法的PC寄存器的值
- 一个方法的结束,有两种方式。一种是正常执行完成,另一种是出现未处理的异常,非正常退出。无论通过哪种方式退出,在方法退出后都返回到该方法被调用的位置。方法正常退出时,调用者的pc计数器的值作为返回地址,即调用该方法的指令的下一条指令的地址。而通过异常退出时,返回地址是要通过异常表来确定,栈帧中一般不会保存这部分信息
- 本质上,方法的退出就是当前栈帧出栈的过程。此时,需要恢复上层方法的局部变量表、操作数栈、将返回值压入调用者栈帧的操作数栈、设置PC寄存器值等,让调用者方法继续执行下去
- 正常完成出口和异常完成出口的区别在于,通过异常完成出口退出的时候不会给他的上层调用者产生任何的返回值
- 一个方法在正常调用完成之后究竟需要使用哪一个返回指令还需要根据方法返回值的实际数据类型而定。在字节码指令中,返回指令包含
ireturn
(当返回值是boolean
、byte
、char
、short
和int
类型时使用)、lreturn
、freturn
、dreturn
以及areturn
,另外还有一个return
指令供声明为void
的方法、实例初始化方法<init>
、类初始化方法<clinit>
和接口的初始化方法使用