简介
每当启动一个新线程时,Java虚拟机都会为它分配一个Java栈。Java栈以帧为单位保存线程的运行状态。虚拟机只会直接对Java栈执行两种操作:以帧为单位的压栈和出栈;
帧存储了方法的局部变量表,操作数栈,动态连接和方法返回地址等信息。
栈帧中需要多大的局部变量表,多深的操作数栈都已经完全确定了,并且写入到了方法表的Code属性中,因此一个栈帧需要分配多少内存,不会受到程序运行期变量数据的影响,而仅仅取决于具体虚拟机的实现;
线程中的方法调用链可能会很长,每个方法都会生成对应的一块栈帧空间,方法以两种方式完成,一种通过return返回的,称为正常返回;一种是通过抛出异常而异常终止的。不管以哪种方式返回,虚拟机都会将当前帧弹出Java栈然后释放掉,这样上一个方法的帧就成为当前帧了。
栈帧的结构
局部变量表(Local Variable Table)
局部变量表是一组变量值存储空间,用于存放方法参数和方法内部定义的局部变量。在Java程序编译为Class文件时,就在方法表的Code属性的max_locals数据项中确定了该方法需要分配的最大局部变量表的容量。
局部变量表的容量以变量槽(Slot)为最小单位,虚拟机规范中未说明它该有多大,只说每个Slot都应该能存放一个boolean、byte、char、short、int、float、reference或returnAddress类型的数据,reference类型表示对一个对象实例的引用,虚拟机对它的长度和结构没有说明。
为了尽可能节省栈帧空间,局部变量表中的Slot是可以重用的,方法体中定义的变量其作用域并不一定会覆盖整个方法体,如果当前字节码程序计数器的值已经超出了某个变量的作用域,那这个变量对应的Slot就可以交给其他变量使用。不过这样的设计虽节省了空间,但也会有一定的副作用,例如在某些情况下,Slot的复用会直接影响到系统的垃圾收集行为。
说了那么多,其实就把它理解为存储当前方法内的局部变量的。
操作数栈(Operand Stack)
操作数栈也常被称为操作栈,它是一个后入先出栈。同局部变量表一样,操作数栈的最大深度也是编译的时候被写入到方法表的Code属性的 max_stacks数据项中。操作数栈的每一个元素可以是任意Java数据类型,包括long和double。32位数据类型所占的栈容量为1,64位 数据类型所占的栈容量为2。栈容量的单位为“字宽”,对于32位虚拟机来说,一个”字宽“占4个字节,对于64位虚拟机来说,一个”字宽“占8个字节。
当一个方法刚刚执行的时候,这个方法的操作数栈是空的,在方法执行的过程中,会有各种字节码指向操作数栈中写入和提取值,也就是入栈与出栈操作。例如,在做算术运算的时候就是通过操作数栈来进行的,又或者调用其它方法的时候是通过操作数栈来行参数传递的。
另外,在概念模型中,两个栈帧作为虚拟机栈的元素,相互之间是完全独立的,但是大多数虚拟机的实现里都会作一些优化处理,令两个栈帧出现一部分重叠。让下 栈帧的部分操作数栈与上面栈帧的部分局部变量表重叠在一起,这样在进行方法调用返回时就可以共用一部分数据,而无须进行额外的参数复制传递了。
我们可以通过一段代码看下
public class Match {
public static final int initData = 666;
public static Sat sat = new Sat();
public byte[] arr = new byte[1024 * 25];
public int compute(){
int a = 1;
int b = 2;
int c = (a + b) * 100;
return c;
}
public static void main(String[] args) throws Exception {
Match math = new Match();
int result = math.compute();
System.out.println(result);
}
}
如果我们执行上述代码,此线程开始到此线程结束,共执行了两个方法,该线程对应的也就是两个栈帧,同数据结构栈一致,也是遵循先进后出的方式进行栈帧的加载。
通过字节码反汇编后
Compiled from "Match.java"
public class com.kxl.e.invoice.admin.utils.Match {
public static final int initData;
public static com.kxl.e.invoice.admin.biz.cert.dao.entity.Sat sat;
public byte[] arr;
public com.kxl.e.invoice.admin.utils.Match();
Code:
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: aload_0
5: sipush 25600
8: newarray byte
10: putfield #2 // Field arr:[B
13: return
public int compute();
Code:
0: iconst_1 //iconst_1 将int类型常量1压入栈
1: istore_1 //istore_1 将int类型值存入局部变量1
2: iconst_2 //iconst_2 将int类型常量2压入栈
3: istore_2 //istore_2 将int类型值存入局部变量2
4: iload_1 //iload_1 从局部变量1中装载int类型值
5: iload_2 //iload_2 从局部变量2中装载int类型值
6: iadd //iadd 执行int类型的加法
7: bipush 100 //bipush 将一个8位带符号整数压入栈
9: imul //mul 执行int类型的乘法
10: istore_3 //istore_3 将int类型值存入局部变量3
11: iload_3 //iload_3 从局部变量3中装载int类型值
12: ireturn //ireturn 从方法中返回int类型的数据
public static void main(java.lang.String[]) throws java.lang.Exception;
Code:
0: new #3 // class com/kxl/e/invoice/admin/utils/Match
3: dup
4: invokespecial #4 // Method "<init>":()V
7: astore_1
8: aload_1
9: invokevirtual #5 // Method compute:()I
12: istore_2
13: getstatic #6 // Field java/lang/System.out:Ljava/io/PrintStream;
16: iload_2
17: invokevirtual #7 // Method java/io/PrintStream.println:(I)V
20: return
static {};
Code:
0: new #8 // class com/kxl/e/invoice/admin/biz/cert/dao/entity/Sat
3: dup
4: invokespecial #9 // Method com/kxl/e/invoice/admin/biz/cert/dao/entity/Sat."<init>":()V
7: putstatic #10 // Field sat:Lcom/kxl/e/invoice/admin/biz/cert/dao/entity/Sat;
10: return
}
具体细节可以参照“JVM指令手册”
地址://www.greatytc.com/p/53a052adffc1
操作架构图:
0: iconst_1 //iconst_1 将int类型常量1压入栈
1: istore_1 //istore_1 将int类型值存入局部变量1
4: iload_1 //iload_1 从局部变量1中装载int类型值
5: iload_2 //iload_2 从局部变量2中装载int类型值
6: iadd //iadd 执行int类型的加法
7: bipush 100 //bipush 将一个8位带符号整数压入栈
9: imul //mul 执行int类型的乘法
10: istore_3 //istore_3 将int类型值存入局部变量3
动态链接
符号引用和直接引用在运行时进行**解析和链接的过程,叫动态链接
一个方法调用另一个方法,或者一个类使用另一个类的成员变量时,需要知道其名字
符号引用就相当于名字,这些被调用者的名字就存放在Java字节码文件里(.class 文件)
名字知道了,但是Java真正运行起来的时候,如何靠这个名字(符号引用)找到相应的类和方法
需要解析成相应的直接引用,利用直接引用来准确地找到。
方法出口
当代码执行到26行的时候,进入compute方法之前执行该指令,将 compute方法执行完毕之后执行的代码行号保存到 compute方法对应" 栈帧 " 中的方法出口中,compute方法的 " 方法出口 " 是第 26 行代码!
帧数据区
帧数据区的大小依赖于 JVM 的具体实现
程序计数器
指向当前线程所执行的字节码指令(地址)行号。
程序计数器由字节码执行引擎控制操作。
注意:为什么字节码中没有行数8 ,其实100 对应的就是8,只不过是一个隐形的行数
本地方法栈
本地方法栈和虚拟机栈所发挥的作用时非常相似的,虚拟机栈为虚拟机执行Java方法服务,本地方法栈则为虚拟机使用到Native方法服务,虚拟机规范中对本地方法栈中的方法使用的语言、使用方式与数据结构并没有强制规定,因此具体的虚拟机可以自由实现它。
Navtive 方法是 Java 通过 JNI 直接调用本地 C/C++ 库,可以认为是 Native 方法相当于 C/C++ 暴露给 Java 的一个接口,Java 通过调用这个接口从而调用到 C/C++ 方法。当线程调用 Java 方法时,虚拟机会创建一个栈帧并压入 Java 虚拟机栈。然而当它调用的是 native 方法时,虚拟机会保持 Java 虚拟机栈不变,也不会向 Java 虚拟机栈中压入新的栈帧,虚拟机只是简单地动态连接并直接调用指定的 native 方法。