【JVM系列】Java字节码的执行栈------虚拟机栈详解
欢迎关注,分享更多原创技术内容~
微信公众号:ByteRaccoon、知乎:一只大狸花啊、稀土掘金:浣熊say
微信公众号海量Java、数字孪生、工业互联网电子书免费送~
栈与栈帧
我们知道当程序运行的时候,需要一个"栈"的数据结构来完成对源代码转译而来的操作指令(JVM的操作指令)的压栈和出栈等操作,来实际运行程序。在Java中程序的运行一般是以线程为单位的,而虚拟机栈就是Java线程专属的栈空间。每个线程创建之后都会单独申请自己独立的一个栈空间,每个方法的调用都会对应着一个栈帧,栈帧包含局部变量表、操作数栈、动态连接、方法出口等。
在 Java 虚拟机中,每个线程存在着一个虚拟机栈,每个方法的执行都对应着一个栈帧,栈帧的入栈和出栈过程,以及栈帧之间的切换和返回值传递。栈和栈帧是实现方法调用和执行的重要机制:
- 栈与栈帧:
- 栈: Java 虚拟机栈是线程私有的,用于存储线程执行方法时的局部变量 、操作数栈、动态链接、方法出口等信息,每个线程都有自己的 Java 虚拟机栈。
- 栈帧: 每个方法的执行对应一个栈帧。栈帧包含了局部变量表、操作数栈、动态链接、方法出口等信息。每次方法调用时都会创建一个新的栈帧,该栈帧被推入虚拟机栈,成为当前活动栈帧。
- 栈帧的入栈和出栈:
- 入栈: 当一个方法被调用时,会在虚拟机栈顶创建一个新的栈帧,该栈帧成为当前活动栈帧,PC 寄存器指向当前活动栈帧的地址,执行方法的指令序列从该地址开始。
- 出栈: 当方法执行完成时,对应的栈帧会被移除,控制权回到前一个栈帧。前一个栈帧中的返回值成为当前活动栈帧的一个操作数,继续执行。
- 栈帧的切换:
- 当一个方法调用其他方法时,会创建一个新的栈帧,该栈帧成为当前活动栈帧,只有当前活动栈帧中的局部变量才能被使用,其他栈帧中的局部变量不可访问。
- 当新的栈帧中的方法执行完毕后,该栈帧被移除,之前的栈帧重新成为活动栈帧,继续执行。
栈帧
Java 虚拟机中的栈帧(Stack Frame)是虚拟机运行时数据区中虚拟机栈的栈元素,用于支持虚拟机进行方法调用和方法执行,每个方法在执行时都对应着一个栈帧。一个方法从调用开始到执行完成,对应着一个栈帧在虚拟机栈中从入栈到出栈的过程。而当前栈帧则是执行引擎关注的有效栈帧,决定了当前执行的方法。
栈帧的组成 包括了局部变量表、操作数栈、动态连接、方法返回地址等信息。在编译代码时,局部变量表和操作数栈的大小已经确定,并写入方法表的 Code 属性中。因此,栈帧所需的内存不受程序运行期变量数据的影响,仅取决于虚拟机的实现。
就栈帧的状态来说,一个线程中可能存在很多方法调用,方法之间相互调用,形成一个方法调用链。对于JVM来说,只有当前栈帧才是有效的,被称为当前栈帧(Current Stack Frame)。当前栈帧关联的方法称为当前方法(Current Method)。JVM只针对当前栈帧进行操作,执行引擎运行的所有字节码指令都是针对当前栈帧进行的。
局部变量表
局部变量表(Local Variable Table)是Java虚拟机中的一块内存区域,用于存储方法执行过程中的局部变量。每个方法在执行时都会创建一个栈帧(Stack Frame),栈帧中包括局部变量表、操作数栈、动态链接、方法返回地址等信息。局部变量表就是栈帧中的一部分,用于存储方法中定义的局部变量。以下是局部变量表的一些关键特点:
- 存储局部变量: 局部变量表用于存储方法中的局部变量,包括方法参数和在方法体内部定义的局部变量。系统不会为局部变量赋予初始值,与实例变量和类变量不同,局部变量在准备阶段不会被赋予初始值。
- 数据类型: 局部变量表中的每个槽位可以存储一个数据,而数据的类型可以是各种基本数据类型(如int、float、double、long等)以及对象引用。局部变量表的存储以变量槽(Slot)为最小单位。在32位虚拟机中,一个Slot可以存放32位(4字节)以内的数据类型,包括boolean、byte、char、short、int、float、reference和returnAddress等八种类型。对于64位长度的数据类型(如long和double),虚拟机以高位对齐方式为其分配两个连续的Slot空间,相当于将一次long和double数据类型读写分割成两次32位读写。并且,Slot是可以重用的,当Slot中的变量超出作用域时,下一次分配Slot时会覆盖原有的数据。对于对象的引用存储在Slot中,会影响垃圾回收,即如果被引用,对象将不会被回收
- 编译时确定: 局部变量表的容量在编译时就已经确定了,因为它与方法的代码结构直接相关。每个局部变量表的槽位可以存储一个数据,而槽位的索引从0开始。
- 作用域: 局部变量表的作用域仅限于方法体内,当方法执行结束时,局部变量表的内容会被销毁。
- 访问方式: 局部变量表中的局部变量可以通过索引访问。例如,
iload_1
指令表示将索引为1的局部变量加载到操作数栈中。
以下是add方法和相应的字节码,说明了局部变量表的使用:
java
public int add(int a, int b) {
int result = a + b;
return result;
}
使用javac命令编译之后相应的字节码包含以下的指令(具体编译字节码的方法可以参考这里):
assembly
stack=2, locals=4, args_size=3
0: iload_1
1: iload_2
2: iadd
3: istore_3
4: iload_3
5: ireturn
当add方法执行之前,JVM会将add方法构造成一个栈帧推入main线程虚拟机栈中,而参数a和b会作为局部变量,被放置到局部变量表当中。而iload_1和iload_2会分别从局部变量表中读取第一个和第二个元素进行计算。
而add方法是Demo类的成员函数,所以add方法的局部变量表中会默认带一个this指针并放在0号索引位置。参数a和b会按照位置依次被放到局部变量表中。
操作数栈
在Java虚拟机中,操作数栈(Operand Stack)是一个用于存储操作数的临时存储区域。它是虚拟机栈的一部分,用于执行方法时保存中间结果、参数和返回值。
在方法执行的过程中,操作数栈被用于执行各种字节码指令。每个线程都有自己的虚拟机栈,而每个方法在执行时都会有一个对应的栈帧(Stack Frame),栈帧中包含了局部变量表、操作数栈以及一些额外的信息。以下是操作数栈的一些关键特性:
- 栈的结构: 操作数栈是一个后进先出(LIFO)的栈结构,类似于数据结构中的栈。最后压入栈的元素最先被弹出。
- 存储操作数: 操作数栈主要用于存储方法执行时需要操作的数据,包括方法参数、中间计算结果以及方法返回值。
- 字节码执行: 在执行方法时,字节码中的各种指令会涉及到对操作数栈的读取和写入操作。例如,将局部变量加载到操作数栈、进行算术运算、方法调用等都涉及到对操作数栈的操作。
- 临时存储: 操作数栈的内容是临时性的,它在方法执行期间用于暂存数据。一旦方法执行结束,操作数栈的内容也随之被销毁。
如下面的Java代码,这里简单实现了一个add的方法:
java
public int add(int a, int b) {
int result = a + b;
return result;
}
使用javac命令编译之后相应的字节码包含以下的指令(具体编译字节码的方法可以参考这里):
assembly
iload_1 // 将局部变量 a 推入操作数栈
iload_2 // 将局部变量 b 推入操作数栈
iadd // 从操作数栈中弹出两个值,相加,将结果推入操作数栈
istore_3 // 将操作数栈的结果存储到局部变量 result
ireturn // 将结果从方法中返回
在这个例子中,操作数栈用于存储局部变量 a 和 b 的值,并在 iadd 指令中执行加法操作,将结果存储到局部变量 result 中。ireturn 指令将最终结果从方法中返回。整个过程中,操作数栈充当了中间计算结果的存储区域,具体如下列图:
- iload_1 和 iload_2 会分别将局部变量表中索引为1和索引为2的变量压入操作数栈,为后面进行运算做准备。
- iadd操作会将操作数栈顶的两个元素出栈,并给到CPU(这里并不严谨,逻辑上可以这样理解)进行加法运算,运算的结果再压回操作数栈;
- istore操作会将操作数栈顶的元素出栈,并将该元素放回局部变量表;
- ireturn操作会返回一个int值,就是我们刚刚计算出的c=3;
动态连接
在Java源文件被编译成为字节码文件的时候,所有的变量和方法都会被转换成符号引用保存在class文件的常量池当中。描述一个方法调用另外一个方法的时候,就是通过常量池中指向方法的符号引用来进行表示的。
在Java虚拟机栈帧中,每个方法的栈帧都包含了一部分用于存储动态连接信息的数据结构,具体来说存储的是"指向运行时常量池中该栈帧所属方法的引用"------动态连接。
"动态连接"可以使得Java在运行过程中能够快速的获取方法相关的信息,这些信息包括方法所属的类、方法的字节码索引、字段和方法的符号引用等。在运行时,当调用方法或访问字段时,这些符号引用会在动态连接的过程中被解析为实际的内存地址,从而快速的完成方法调用或字段访问的操作。
动态连接在Java虚拟机中是为了支持多态性、延迟解析和提高程序的灵活性而设计的,这使得在程序运行时期进行动态绑定和解析,而不是在编译时期确定所有的引用关系。
方法返回地址
方法返回地址是指在方法执行过程中,记录了方法调用点的地址,以便在方法执行完成后能够回到调用它的地方。在 Java 虚拟机的栈帧中,方法返回地址通常是存储在栈帧数据结构的一部分,用于指示方法返回的位置。
当一个方法被调用时,调用点的地址会被记录下来,然后方法开始执行。在方法执行的过程中,如果遇到返回语句,虚拟机会利用保存的方法返回地址回到调用点,继续执行调用者方法的下一条指令。
在栈帧中,方法返回地址的保存方式依赖于具体的虚拟机实现和硬件架构。通常,它会被保存在栈帧的特定位置,以确保在方法返回时能够被正确地取出和使用。
需要注意的是,方法返回地址通常是指明下一条指令的地址,而不是直接指向调用语句的地址。这是因为在方法执行期间可能会有一些中间操作,如异常处理、同步块的释放等,因此返回地址不仅仅是简单的调用点地址。
总结
虚拟机栈是Java线程运行的时候存储运行方法的区域,一般每个Java线程都会有专属的虚拟机栈。虚拟机栈由很多栈帧构成,当一个方法执行的时候就会向虚拟机栈中压入一个栈帧,栈顶的栈帧就是当前的活动栈帧。
栈帧一般由局部变量表、操作数栈、动态连接和方法返回地址等构成,局部变量表主要存储方法所需要用到的局部变量;操作数栈则是将需要进行运算的操作数进行压栈和出栈操作;动态连接主要是在程序运行期间将常量池中的符号引用映射到真实的物理地址的操作,方便快速的访问栈帧对应方法的属性;方法返回地址则是调用方法的代码的物理内存地址。