栈的分类
栈分为Java虚拟机栈还有本地方法栈:
- Java虚拟机栈:用于保存Java中的方法相关的内容
- 本地方法栈:用于保存在Java中使用native 标记的用C++来实现方法
由于hotspot的作者发现使用一个栈就可以保存以上两个部分的内容,所以在hotspot只有一个栈
Java虚拟机栈
Java虚拟机栈使用了先进后出的数据结构,**每个方法都用一个栈帧来保存,栈帧存储了方法的局部变量表、操作数栈、动态连接和方法返回地址等信息。每一个方法从调用至执行完成的过程,都对应着一个栈帧在虚拟机栈里从入栈到出栈的过程。**栈是线程不能共享的,所以每个线程都有自己的虚拟机栈
我们可以通过idea 中的Frames 来查看当前栈帧,这里就不详细说明了,具体请查看文章:通过idea 中的Frames 来查看当前栈帧
局部变量表(本地变量表)
成员变量:在类中声明的变量,也称为实例变量。 每个对象都有自己的一份成员变量副本,它们存储在堆内存中,并且可以被类的所有方法访问。
局部变量:在方法、构造函数或代码块中声明的变量,只能在其所属的作用域内使用。 局部变量存储在栈内存中,当其所属的方法执行完毕后会被销毁。
局部变量表分为class 文件中的局部变量表和运行时的局部变量表,而运行时数据区的局部变量表则是运行时局部变量表,
-
运行时的局部变量表:
- 栈帧中的局部变量表,是一个数组,最基本的存储单元为slot(变量槽)
- 参数值的存放总在局部变量数组的index 0 开始,到数组长度-1的索引结束。
- 局部变量表中存放编译器可知的各种基本数据类型(8种),引用类型(reference),returnAddress类型的变量,
- 在局部变量表中 32位以内的类型只占用一个slot(包括上面提到的returnAddress类型),64位的类型占用两个.
- byte short char在存储前被转化为int , boolean在存储前 也被转化为int , 0 表示false,非0表示true
- long和double类型占用两个槽,其他类型占用一个槽。
- 通过class文件中的局部变量表可以确定方法加载到内存中时需要分配多少的内存空间,其实就是计算当前方法需要占用多少个槽
-
class文件中的局部变量表包含如下内容:
NR: 局部变量表的编号,例如:下图中方法A在方法的最开始,那么它的编号是0,其次是 ”i" 的编号是1,以此类推
**起始PC:**PC其实就是程序计数器,也就是当前变量或方法等可以在字节码指令可以访问的起始位置
**长度:**就以下方截图的main方法为例,改方法的字节码如下,首先这里的iconst_0,istore_1,istore_2中的0,1,2应该是局部变量表中NR的值,0代表注释掉的方法A,1代表变量i,2代表变量j,由此我们可以看出以变量"i"为例,i在第二行开始执行,也就是下方为1的位置,在第六行结束也就是下方为5的位置,所以这里的起始pc为2,长度为5
**序号:**对应局部变量表中变量的起始位置,也就是“槽”的位置,假如有一个变量为 ** i **则在局部变量表中的序号为0,则变量 i 在局部变量表中槽的位置为0
0 iconst_0 1 istore_1 2 iconst_0 3 istore_2 4 iconst_1 5 istore_1 6 return
操作数栈
操作数栈是栈帧中虚拟机在执行指令过程中用来存放中间数据的一块区域。他是一种栈式的数据结构,如果一条指令将一个值压入操作数栈,则后面的指令可以弹出并使用该值。在编译期就可以确定操作数栈的最大深度,从而在执行时正确的分配内存大小。例如:
- 有一个变量 i 等于 0 需要执行i+3
- 首先会将0的值放入操作数栈
- 由于此时0没有了后续的计算操作,所以将0弹出栈并放入内存中赋值给i
- 此时 i 的值等于0,再将0的值放入操作数栈
- 之后再将3放入操作数栈,两个值相加后,进行弹栈,并将结果值赋值给局部变量表中的 i 字段
帧数据
在JVM(Java虚拟机)中,一个栈帧(Stack Frame)确实对应着一个方法的执行。每当一个方法被调用时,JVM 会为该方法创建一个新的栈帧,并将其压入当前线程的调用栈(Java虚拟机栈)的顶部。这个栈帧包含了方法执行所需的信息,包括:
- 局部变量表(Local Variables):用于存储方法参数和方法内部定义的局部变量。
- 操作数栈(Operand Stack):用于执行方法内的运算。
- 动态链接(Dynamic Linking):方法可能需要访问其他方法或变量,这部分信息帮助完成这些访问。
- 返回地址(Return Address):方法执行完毕后,控制权需要返回到哪个指令,这个信息由返回地址记录。
返回地址(Return Address)在JVM的上下文中,是指向程序计数器(Program Counter Register)的下一条指令的内存位置。当一个方法被调用时,JVM会保存当前线程的程序计数器的值作为返回地址,这个值指示了在调用当前方法之前,线程正在执行的指令的下一条位置。
当方法执行完毕,无论是正常返回还是通过异常处理机制退出,JVM都会使用这个返回地址来更新程序计数器,使得线程能够从上次中断的地方继续执行。这样,程序就可以按照预期的顺序继续执行,或者在异常处理完成后跳转到适当的处理代码。
需要注意的是,JVM规范并没有强制要求使用程序计数器来实现返回地址,而是允许实现者选择其他方式来跟踪方法的执行流程。但通常情况下,返回地址与程序计数器的概念是紧密相关的 - 常量池引用(Constant Pool Reference):指向方法所属类的运行时常量池,用于解析符号引用。
当方法执行完毕(无论是正常返回还是抛出异常),相应的栈帧会被弹出,虚拟机栈恢复到调用该方法前的状态,程序继续执行调用该方法之后的下一条指令。因此,栈帧是JVM实现方法调用、方法执行及结果返回过程中的核心数据结构之一。
。