1 虚拟机栈(Java Virtual Machine Stack)简介
由于JVM的跨平台的设计,Java的指令都是根据栈来设计的。不同平台CPU架构不同,所以不能设计为基于寄存器的。JVM中栈管运行,堆管存储
每个线程在创建时都会创建一个虚拟机栈,该虚拟机栈是线程私有的,生命周期和线程一致, 其内部保存一个个的栈帧(Stack Frame),对应一次次的Java方法调用
public class StackTest {
public void methodA() {
int i = 13;
int j = 20;
methodB();
}
public int methodB() {
int k = 20;
int m = 20;
return k + m;
}
public static void main(String[] args) {
StackTest stackTest = new StackTest();
stackTest.methodA();
}
}
上述代码主线程的虚拟机栈的结构如图
2. 虚拟机栈的作用
对于栈的操作,就是一个出栈入栈的操作,因此我们需要特别关注虚拟机栈的栈顶,最上面的方法就是当前方法,最上面的操作就是当前正在执行的操作,每次方法的调用就是一个个栈帧的入栈出栈
在虚拟机栈中,保存了方法的局部变量(8大基本类型,对象地址)和部分结果,并参与方法的调用和返回。
虚拟机栈的访问速度仅次于程序计数器,且不存在垃圾回收问题。
3. 栈帧 Stack Frame
每个线程都有自己的虚拟机栈,栈中的数据都是以栈帧的格式存在的,在这个线程上正在执行的每个方法都各自对应一个栈帧,栈帧就是存储在用户栈上的(当然内核栈同样适用)每一次函数调用涉及的相关信息的记录单元集。
一个运行的线程拥有自己的虚拟机栈,一个时间点上,只会有一个“活动”的栈帧,即只有栈顶的栈帧是有效的,这个栈帧被称为当前栈帧 Current Frame,与当前栈帧对应的方法就是当前方法 Current Method,定义这个方法的类就是当前类 Current Class
执行引擎运行的所有字节码指令只针对当前栈帧,程序计数器中存储的就是当前栈帧的指令地址
Java方法有两种返回函数的方法,一种是正常的函数返回,使用return指令;另一种是抛出异常,不管使用哪种方式,都会导致栈帧被弹出
正常结束
public class TestStackFrame {
public void methodA() {
System.out.println("开始执行A()");
int i = 20;
methodB();
System.out.println("执行A()结束");
}
public int methodB() {
System.out.println("开始执行B()");
int j = 20;
System.out.println("执行B()结束");
return j;
}
public static void main(String[] args) {
TestStackFrame testStackFrame = new TestStackFrame();
testStackFrame.methodA();
}
}
// 输出结果
开始执行A()
开始执行B()
执行B()结束
执行A()结束
异常不进行处理
public class TestStackFrame {
/*
8 bipush 20
10 istore_1
11 aload_0
12 invokevirtual #5 <com/shang/jvm/runtimedata/stack/TestStackFrame.methodB : ()I>
15 pop
16 getstatic #2 <java/lang/System.out : Ljava/io/PrintStream;>
19 ldc #6 <执行A()结束>
21 invokevirtual #4 <java/io/PrintStream.println : (Ljava/lang/String;)V>
24 return
*/
public void methodA() {
System.out.println("开始执行A()");
int i = 20;
methodB();
System.out.println("执行A()结束");
}
public int methodB() {
System.out.println("开始执行B()");
int j = 20;
System.out.println("执行B()结束");
return j/0;
}
public static void main(String[] args) {
TestStackFrame testStackFrame = new TestStackFrame();
testStackFrame.methodA();
}
}
开始执行A()
开始执行B()
执行B()结束
Exception in thread "main" java.lang.ArithmeticException: / by zero
扑获异常
public class TestStackFrame {
/*
11 aload_0
12 invokevirtual #5 <com/shang/jvm/runtimedata/stack/TestStackFrame.methodB : ()I>
15 pop
16 goto 24 (+8) 无条件跳转至第24行
19 astore_2 将异常信息放入局部变量表索引为2的地方
20 aload_2 获取异常信息
21 invokevirtual #7 <java/lang/Exception.printStackTrace : ()V>
24 getstatic #2 <java/lang/System.out : Ljava/io/PrintStream;>
27 ldc #8 <执行A()结束>
29 invokevirtual #4 <java/io/PrintStream.println : (Ljava/lang/String;)V>
32 return
*/
public void methodA() {
System.out.println("开始执行A()");
int i = 20;
try {
methodB();
} catch (Exception e) {
e.printStackTrace();
}
System.out.println("执行A()结束");
}
public int methodB() {
System.out.println("开始执行B()");
int j = 20;
System.out.println("执行B()结束");
return j/0;
}
public static void main(String[] args) {
TestStackFrame testStackFrame = new TestStackFrame();
testStackFrame.methodA();
}
}
//运行结果
开始执行A()
开始执行B()
执行B()结束
执行A()结束
java.lang.ArithmeticException: / by zero
从上面可以看出,如果对异常没有扑获,直接跳出当前栈帧,一直寻找到可以处理该异常的栈帧处,如果没有处理,则栈帧一个个的被抛出,程序结束。
本质上,方法的退出就是当前栈帧出栈的过程,此时需要恢复调用者的局部变量表,操作数栈,将返回值压入调用者栈帧的操作数栈,设置程序计数器的值,让调用者继续执行下去
3.1 栈帧的结构
- 局部变量表(Local Variables)
- 操作数栈(Operand Stack)
- 动态链接(Dynamic Linking)
- 方法返回地址(Return Address)
- 一些附加信息
3.2 局部变量表Local Variable Table
局部变量表(Local Variable Table)是栈帧的组成部分,栈帧从属于虚拟机栈,所以局部变量表是线程的私有数据,不存在数据安全问题,局部变量表所需的容量大小是在编译期确定下来的,不可更改
局部变量表是一个数组,局部变量表最基本的存储单元是变量槽Slot,主要用于存储方法参数和定义在方法内部的局部变量,这些数据包括基本数据类型,对象引用(堆中实例的地址),以及returnAddress类型
32位以内的类型只占用一个Slot(除了long和double外的所有类型),64位的类型(long和double)占用两个Slot。
JVM会为局部变量表中的每一个slot分配一个访问索引,通过这个索引即可成功访问到局部变量表中指定的局部变量值。
如图所示:
i的长度占了1个slot, j的长度占了2给slot。
this也是一个变量,可以在构造方法和普通方法中使用,但不能用于静态方法中,因为静态方法的局部变量表不会存储this变量
局部变量表中的变量是当前方法私有的,在方法执行时,虚拟机通过使用局部变量表完成参数值到参数变量列表的传递过程,当方法调用结束后,随着方法栈帧的销毁,从属的局部变量表也会销毁
局部变量表也与垃圾回收相关,局部变量表中直接或间接引用的对象都不会被回收
3.3 操作数栈 Operand Stack
每一个独立的栈帧中,除了局部变量表还包含一个先进后出的操作数栈,操作数栈随着当前方法开始执行时而创建,其最大深度在编译期就已定义好,32位类型占用一个栈单位深度,64位类型占用两个栈单位深度。
Java虚拟机的解释引擎是基于栈的执行引擎,栈指的就是操作数栈,执行引擎解释执行字节码指令时,执行add,store,load等操作时,都会从局部变量表和常量池中取值进行出栈相加/相乘等操作,再存储到局部变量表中,从而执行完整个方法
对下面的方法的字节码进行分析:
public void operandStack() {
int i = 10;
int j = 155;
int k = i + j;
}
/**
* 0 bipush 10 将10放入操作数栈中,因为 10满足byte的大小,所以将10作为byte类型放入操作数栈
* 2 istore_1 将操作数栈的栈顶元素以int类型出栈,放入局部变量表下标为1的地方 =》即对应着i = 10
* 3 sipush 155 将155放入操作数栈中,因为 155满足short的大小,所以将155作为short类型放入操作数栈
* 6 istore_2 将操作数栈的栈顶元素以int类型出栈,放入局部变量表下标为2的地方 =》即对应着j = 155
* 7 iload_1 将局部变量表下标为1的数以int类型放入栈中
* 8 iload_2 将局部变量表下标为2的数以int类型放入栈中
* 9 iadd 取出栈中最上面的两个变量以进行相加,并以int类型放回到栈中
* 10 istore_3 将操作数栈的栈顶元素以int类型出栈,放入局部变量表下标为3的地方 =》即对应着 k = 165
* 11 return 结果返回
*/
JVM中执行Java方法就是通过执行引擎执行对应方法的字节码指令,而执行一个方法需要对方法中的参数和变量进行运算并返回指定的结果,局部变量表就负责存储各个变量的值,而操作数栈负责对这些变量运算更新,以此来执行完整个方法
在操作数栈中,操作数是存储在内存中的,因此频繁地进行内存的读/写操作,会降低执行引擎的工作效率,为此HotSpot VM提供了栈顶缓存技术,即将栈顶数据全部缓存到物理CPU的寄存器中,以此降低了对内存的频繁读/写,提升了执行引擎的执行效率
3.4 动态链接 Dynamic Linking
在Java源文件被编译成字节码文件时,方法引用都作为符号引用保存到class文件的常量池,其作用就是为了提供一些符号和常量,便于指令的识别,当Java程序启动后,在方法区中生成运行时常量池,运行时常量池保存符号引用对应的直接引用
示例如下:
在getReturn()中调用了虚方法testReturn(),在指令中可以看见这条指令:
invokevirtual #5 <com/shang/jvm/runtimedata/stack/OperandStack.testReturn : ()I>
#5在常量池中对应内容
invokevirtual #5 实际上就是执行<com/shang/jvm/runtimedata/stack/OperandStack.testReturn : ()I>
#5是常量池中的符号引用,其对应的直接引用是com.shang.jvm.runtimedata.OperandStack.testReturn ()
当getReturn()需要调用testReturn()时,需要通过动态链接在运行时常量池找到method2()的直接引用。
动态链接的作用就是从运行时常量池中找到被调用虚方法的直接引用,以便执行引擎来执行该虚方法
3.5 方法的调用
3.5.1 静态链接和动态链接
将被调用方法的符号引用转换为直接引用有两种方式,转换的时机不同:
静态链接:当一个字节码文件被装载进JVM后,如果被调用的目标方法在编译期可知,且运行期保持不变,这种情况下将被调用方法的符号引用转换为直接引用的过程称为静态链接
动态链接:如果被调用方法在编译期无法被确定下来,即只能在程序运行时,将被调用方法的符号引用转换为直接引用的过程称为动态链接
3.5.2 早期绑定和晚期绑定
在JVM中,将被调用方法的符号引用转换为直接引用的时机与方法的绑定机制有关,对应的绑定机制分为早期绑定和晚期绑定
绑定是一个字段,方法,或者类的符号引用被替换为直接引用的过程,该过程只会发生一次
早期绑定: 指被调用的目标方法在编译期可知,且运行期间保持不变,
即可以将该方法与所属的类进行绑定,这样一来,由于确定了被调用的目标方法是哪个,因此也就可以使用静态链接的方式将符号引用转换为直接引用
晚期绑定: 如果被调用的方法在编译期无法被确定下来,只能根据程序运行时根据实际的类绑定相关的方法,进行动态链接,这种方式称为晚期绑定
3.5.3 非虚方法和虚方法
如果方法在编译期就确定了具体的调用版本,该版本在运行时也不可变(不能多态执行),这样的方法称为非虚方法,非虚方法进行早期绑定,静态链接
能多态执行的方法,编译时不能确定版本的称为虚方法,虚方法进行晚期绑定,动态链接
3.5.4 JVM中方法的调用指令
普通调用指令
1,invokestatic 调用静态方法 (非虚方法)
2,invokespecial 调用<init>构造方法 (非虚方法)
3,invokevirtual 调用虚方法 (虚方法)
(final方法被调用时也是invokevirtual 是特例 final方法本质还是非虚方法)
4,invokeinterface 调用接口方法 (虚方法)
这4条指令在JVM中被固化,方法的调用不可人为干涉
动态调用执行
5,invokedynamic 动态解析出需要调用的方法,然后执行 (虚方法)
该指令支持用户确定方法版本