介绍
java虚拟机栈是线程私有的,他与线程的声明周期同步。虚拟机栈描述的是java方法执行的内存模型,每个方法执行都会创建一个栈帧,栈帧包含局部变量表、操作数栈、动态连接、方法出口等。
栈与栈帧
每一个方法的执行到执行完成,对应着一个栈帧在虚拟机中从入栈到出栈的过程。java虚拟机栈栈顶的栈帧就是当前执行方法的栈帧。PC寄存器会指向该地址。当这个方法调用其他方法的时候久会创建一个新的栈帧,这个新的栈帧会被方法Java虚拟机栈的栈顶,变为当前的活动栈,在当前只有当前活动栈的本地变量才能被使用,当这个栈帧所有指令都完成的时候,这个栈帧被移除,之前的栈帧变为活动栈,前面移除栈帧的返回值变为这个栈帧的一个操作数。
栈帧
栈帧(Stack Frame)是用于支持虚拟机进行方法调用和方法执行的数据结构,它是虚拟机运行时数据区的虚拟机栈(Virtual Machine Stack)的栈元素。栈帧存储了方法的局部变量表,操作数栈,动态连接和方法返回地址等信息。第一个方法从调用开始到执行完成,就对应着一个栈帧在虚拟机栈中从入栈到出栈的过程。 每一个栈帧都包括了局部变量表,操作数栈,动态连接,方法返回地址和一些额外的附加信息。在编译代码的时候,栈帧中需要多大的局部变量表,多深的操作数栈都已经完全确定了,并且写入到了方法表的Code属性中,因此一个栈帧需要分配多少内存,不会受到程序运行期变量数据的影响,而仅仅取决于具体虚拟机的实现。 一个线程中的方法调用链可能会很长,很多方法都同时处理执行状态。对于执行引擎来讲,活动线程中,只有虚拟机栈顶的栈帧才是有效的,称为当前栈帧(Current Stack Frame),这个栈帧所关联的方法称为当前方法(Current Method)。执行引用所运行的所有字节码指令都只针对当前栈帧进行操作。栈帧的概念结构如下图所示:

局部变量表
- 局部变量表是变量值的存储空间,用于存放方法参数和方法内部定义的局部变量。在java编译成class文件的时候,就在方法的Code属性的max_locals数据项中确定该方法需要分配的最大局部变量表的容量。
- 局部变量表的容量以变量槽(Slot)为最小单位,32位虚拟机中一个Slot可以存放32位(4 字节)以内的数据类型( boolean、byte、char、short、int、float、reference和returnAddress八种)
- 对于64位长度的数据类型(long,double),虚拟机会以高位对齐方式为其分配两个连续的Slot空间,也就是相当于把一次long和double数据类型读写分割成为两次32位读写。
- reference类型虚拟机规范没有明确说明它的长度,但一般来说,虚拟机实现至少都应当能从此引用中直接或者间接地查找到对象在Java堆中的起始地址索引和方法区中的对象类型数据。
- Slot是可以重用的,当Slot中的变量超出了作用域,那么下一次分配Slot的时候,将会覆盖原来的数据。Slot对对象的引用会影响GC(要是被引用,将不会被回收)。 系统不会为局部变量赋予初始值(实例变量和类变量都会被赋予初始值)。也就是说不存在类变量那样的准备阶段。
- 系统不会为局部变量赋予初始值(实例变量和类变量都会被赋予初始值)。也就是说不存在类变量那样的准备阶段。
操作数栈
- 操作数栈和局部变量表一样,在编译时期就已经确定了该方法所需要分配的局部变量表的最大容量。
- 操作数栈的每一个元素可用是任意的Java数据类型,包括long和double。32位数据类型所占的栈容量为1,64位数据类型占用的栈容量为2。
- 当一个方法刚刚开始执行的时候,这个方法的操作数栈是空的,在方法执行的过程中,会有各种字节码指令往操作数栈中写入和提取内容,也就是出栈 / 入栈操作(例如:在做算术运算的时候是通过操作数栈来进行的,又或者在调用其它方法的时候是通过操作数栈来进行参数传递的)。
- 在概念模型里,栈帧之间是应该是相互独立的,不过大多数虚拟机都会做一些优化处理,使局部变量表和操作数栈之间有部分重叠,这样在进行方法调用的时候可以直接共用参数,而不需要做额外的参数复制等工作。重叠过程如图所示:
动态连接
每个栈帧都包含一个指向运行时常量池中该栈帧所属方法的引用,Class文件的常量池中存有大量的符号引用,字节码中的方法调用指令就以常量池中方法的符号引用为参数。这些符号引用一部分会在类加载阶段或者第一次使用的时候就转化为直接引用(静态方法,私有方法等),这种转化称为静态解析,另一部分将在每一次运行期间转化为直接引用,这部分称为动态连接。由于篇幅有限这里不再继续讨论解析与分派的过程,这里只需要知道静态解析与动态连接的区别就好。
方法返回地址
当一个方法开始执行后,只有两种方式可以退出这个方法:
- 执行引擎遇到任意一个方法返回的字节码指令:传递给上层的方法调用者,是否有返回值和返回值类型将根据遇到何种方法来返回指令决定,这种退出的方法称为正常完成出口。
- 方法执行过程中遇到异常: 无论是java虚拟机内部产生的异常还是代码中thtrow出的异常,只要在本方法的异常表中没有搜索到匹配的异常处理器,就会导致方法退出,这种退出的方式称为异常完成出口,一个方法若使用该方式退出,是不会给上层调用者任何返回值的。无论使用那种方式退出方法,都要返回到方法被调用的位置,程序才能继续执行。方法返回时可能会在栈帧中保存一些信息,用来恢复上层方法的执行状态。一般方法正常退出的时候,调用者的pc计数器的值可以作为返回地址,帧栈中很有可能会保存这个计数器的值作为返回地址。方法退出的过程就是栈帧在虚拟机栈上的出栈过程,因此退出时的操作可能有:恢复上层方法的局部变量表和操作数栈,把返回值压入调用者的操作数栈每条整pc计数器的值指向调用该方法的后一条指令。
实例讲解: java代码:
public class Test {
private static int add(int c){
return c + 10;
}
public static void main(String[] args) {
int a, b, c;
a = 1;
b = 2;
c = add(a*b);
c = c*(a+b);
}
}
使用javap -v 反编译后的
Classfile /E:/Code/JAVA/JUC/out/production/JUC/net/ziruo/juc/Test.class
Last modified 2019-10-29; size 546 bytes
MD5 checksum 3526f85e07771be800502f7e10b50a3a
Compiled from "Test.java"
public class net.ziruo.juc.Test
minor version: 0
major version: 52
flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
#1 = Methodref #4.#24 // java/lang/Object."<init>":()V
#2 = Methodref #3.#25 // net/ziruo/juc/Test.add:(I)I
#3 = Class #26 // net/ziruo/juc/Test
#4 = Class #27 // java/lang/Object
#5 = Utf8 <init>
#6 = Utf8 ()V
#7 = Utf8 Code
#8 = Utf8 LineNumberTable
#9 = Utf8 LocalVariableTable
#10 = Utf8 this
#11 = Utf8 Lnet/ziruo/juc/Test;
#12 = Utf8 add
#13 = Utf8 (I)I
#14 = Utf8 c
#15 = Utf8 I
#16 = Utf8 main
#17 = Utf8 ([Ljava/lang/String;)V
#18 = Utf8 args
#19 = Utf8 [Ljava/lang/String;
#20 = Utf8 a
#21 = Utf8 b
#22 = Utf8 SourceFile
#23 = Utf8 Test.java
#24 = NameAndType #5:#6 // "<init>":()V
#25 = NameAndType #12:#13 // add:(I)I
#26 = Utf8 net/ziruo/juc/Test
#27 = Utf8 java/lang/Object
{
public net.ziruo.juc.Test();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=1, locals=1, args_size=1
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
LineNumberTable:
line 8: 0
LocalVariableTable:
Start Length Slot Name Signature
0 5 0 this Lnet/ziruo/juc/Test;
private static int add(int);
descriptor: (I)I
flags: ACC_PRIVATE, ACC_STATIC
Code:
stack=2, locals=1, args_size=1
0: iload_0
1: bipush 10
3: iadd
4: ireturn
LineNumberTable:
line 12: 0
LocalVariableTable:
Start Length Slot Name Signature
0 5 0 c I
public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=3, locals=4, args_size=1
0: iconst_1
1: istore_1
2: iconst_2
3: istore_2
4: iload_1
5: iload_2
6: imul
7: invokestatic #2 // Method add:(I)I
10: istore_3
11: iload_3
12: iload_1
13: iload_2
14: iadd
15: imul
16: istore_3
17: return
LineNumberTable:
line 18: 0
line 19: 2
line 20: 4
line 21: 11
line 22: 17
LocalVariableTable:
Start Length Slot Name Signature
0 18 0 args [Ljava/lang/String;
2 16 1 a I
4 14 2 b I
11 7 3 c I
}
主要代码注释讲解:
0: iconst_1 (把1压入操作数栈栈顶)
1: istore_1 (把操作数栈栈顶的出栈放入局部变量表索引为1的位置)
2: iconst_2 (把2压入操作数栈栈顶)
3: istore_2 (把操作数栈栈顶的出栈放入局部变量表索引为2的位置)
4: iload_1 (把局部变量表索引为1的值放入操作数栈栈顶)
5: iload_2 (把局部变量表索引为2的值放入操作数栈栈顶)
6: imul (把操作数栈栈顶的和栈顶下面的一个进行乘法运算后放入栈顶)
7: invokestatic #2 // Method add:(I)I (执行静态方法,返回的值放入操作数栈)
10: istore_3 (把操作数栈栈顶的出栈放入局部变量表索引为3的位置)
11: iload_3 把局部变量表索引为3的值放入操作数栈栈顶)
12: iload_1 把局部变量表索引为1的值放入操作数栈栈顶)
13: iload_2 把局部变量表索引为2的值放入操作数栈栈顶)
14: iadd (把操作数栈栈顶的和栈顶下面的一个进行加法运算后放入栈顶)
15: imul (把操作数栈栈顶的和栈顶下面的一个进行乘法运算后放入栈顶)
16: istore_3 (把操作数栈栈顶的出栈放入局部变量表索引为3的位置)
17: return (结束)
参考文章: