虚拟机对方法执行的支持是通过虚拟机栈来实现的。具体地:栈帧入栈代表着方法开始执行,方法执行完成时栈帧出栈。
如下图:main方法开始执行的时候,其对应的栈帧1入栈。在main中调用method1,于是对应的栈帧2入栈。随后,method1调用方法2,对应的栈帧3入栈,方法2执行结束后,栈帧3出栈,此时栈顶是method1的栈帧2。接下来method3对应的栈帧4入栈...
操作数栈
1.运行时栈帧结构
每个栈帧包含了局部变量表、操作数栈、动态链接、方法返回地址和一些额外的信息。
每个栈帧中,局部变量表的大小和操作数栈的最大深度都可以在编译的时候计算出来,存储在class文件中,运行时转化成方法区的数据。
1.1 局部变量表
局部变量表用于存储方法的参数和方法内定义的局部变量。这些变量被存放在变量槽(variable slot)中,每个变量曹都能存放一个bool、byte、short、char、int、float、reference、returnAddress类型的数据。其中reference表示一个实例对象的引用,通过它能够找到该对象在堆内存中的起始地址或索引,同时还能找到对象所属数据类型在方法区存储的类型信息(比如常用的xxx.getClass(),虽然它是找的堆中的Class对象)。returnAddress类型较少见。
如果执行的是实例方法而非static修饰的静态方法,方法的参数实际上还包含了方法所属对象的引用,该引用被存储在局部变量表第0位索引的变量槽。
由于操作数栈是线程私有的,因此对局部变量表内数据的修改不会引发线程安全问题。(当然,作为程序员也没啥机会去修改局部变量表数据)。
在垃圾回收中,局部变量表可以作为一个GC Root。原因很好理解,局部变量表中所引用的对象,在当前时刻都还是有用的,不应当作为垃圾被回收。
1.2 操作数栈
JVM的解释执行引擎是基于栈的执行引擎,这里的栈就是指操作数栈。
对于每一条存数的指令,如iload、ipush...,虚拟机都会将对应的操作数压入操作数栈顶;在取数指令(如 istore...)时,从栈顶弹出对应数量的操作数。当然也有既要取数又要存数的指令,如(iadd,它从栈顶弹出两个操作数,完成假发操作后将结果压入到操作数栈)。
1.3 动态连接
在虚拟机栈中,每个栈帧都包含一个指向运行时常量池中该栈帧所属方法的引用。jvm中方法调用指令将常量池中指向方法的符号引用作为参数,其目的是支持方法调用过程中的动态连接。
所谓方法调用,并不是说方法中的代码被执行,而是确定要调用哪一个方法(即方法版本)。
动态连接,是相对于静态解析而言的。在类加载或第一次使用这些符号引用的时候,会将部分符号引用转成直接引用,这种方式被称为静态解析;比如静态方法和私有方法,前者与类型直接关联,后者在外部不可访问,因此不会有其他版本的方法。还有一部分符号引用要在每次运行期间转化成直接引用,这种方式就是动态连接,比如方法重载与重写。
1.4 方法返回地址
一个方法开始执行后,有两种退出的方式。一种是执行引擎遇到了代表方法返回的字节码指令正常退出,另一种是执行过程中遇到了异常。
无论是否正常退出,都需要返回方法被调用的位置,继续执行后面的程序。正常退出时,主调方法的PC计数器就可以作为返回地址,可以将它存放在栈帧中。但方法异常退出时,返回地址要通过异常处理器表来确定,战阵中通常不会保存该信息。
2. 举个例子
2.1 首先通过一个例子来理解一下基于栈的解释器执行过程。在这里主要关注局部变量表与操作数栈
public static void main(String[] args) {
int a = 100;
int b = a++;
int c = ++a;
// System.out.println("b: " + b + "\n" + "c: " + c);
}
其字节码指令如下idea安装Jclasslib即可查看
0 bipush 100
2 istore_1
3 iload_1
4 iinc 1 by 1
7 istore_2
8 iinc 1 by 1
11 iload_1
12 istore_3
13 return
-
首先是两条表示int a = 100的字节码
执行偏移地址0处的指令,bipush 100。 将单字节整型常量值100压入操作数栈
此时局部变量表和操作数栈如下图
地址0处
执行istore_1,将操作数栈顶的100弹出并存放到局部变量表1的位置。
地址2处执行 -
然后是int b = a++表示的字节码
iload_1,将局部变量表1处的值复制到操作数栈顶
iload_1执行之后
iinc 1 by 1:这条指令的作用是将局部变量表1处的值自增1.执行结束后,如下图
iinc 1 by 1执行之后
istore_2。 将栈顶元素弹出赋给局部变量表2处的值。
istore_2执行完毕 -
接下来进入++a所表示的字节码
iinc 1 by 1,结束后ru
iinc 1 by 1 (地址8处指令)执行结束后
iloa_1:将1处的102压入栈顶
iload_1(地址11处)执行完毕
istore_3执行完毕后,102出栈,复制到局部变量表3处。
istore_3执行完毕后
本方法结束后,局部变量a, b , c的值就存放在局部变量表内。
2.2然后通过一个更复杂一点的例子,来看看
这段代码与上面的一段看起来很像,但是,结果却大不相同。
public class AddA {
public static void main(String[] args) {
int a = 100;
int b = useBeforeIncrement(a);
int c = IncreseBeforeUse(a);
System.out.println("a: "+ a + "\nb: " + b + "\nc: " + c);
}
private static int useBeforeIncrement(int a) {
a = a++;
return a;
}
private static int IncreseBeforeUse(int a) {
a = ++a;
return a;
}
}
------结果------
a: 100
b: 100
c: 101
分析一下字节码指令,便可以看出究竟哪里不同
- main方法对应的字节码指令:
0 bipush 100
2 istore_1
3 iload_1
4 invokestatic #2 <AddA.useBeforeIncrement>
7 istore_2
8 iload_1
9 invokestatic #3 <AddA.IncreseBeforeUse>
12 istore_3
// 后面都是关于print方法的字节码,忽略掉
- useBeforeIncrement(int a)对应的字节码指令
0 iload_0
1 iinc 0 by 1
4 istore_0
5 iload_0
6 ireturn
- IncreseBeforeUse(int a)对应的字节码指令
0 iinc 0 by 1
3 iload_0
4 istore_0
5 iload_0
6 ireturn
首先main方法对应的栈帧压入虚拟机栈。向局部变量表存入100,并复制到操作数栈顶
main方法栈帧
然后调用静态方法AddA类的useBeforeIncrement, 同时取出操作数栈顶的100作为参数传递到AddA.useBeforeIncrement对应栈帧的局部变量表中。
useBeforeIncrement iload_1开始之前
在useBeforeIncrement中,先将0处的100压入操作数栈,然后对局部变量表0处的值自增iinc 0 by 1。接下来执行istore_1,将栈顶的100存回到局部变量表。我们发现,局部变量表内的数据没有发生任何变化,也好理解,因为a = a++是两条指令,a++讲局部变量表0处位置+1,然后赋值又是从栈顶弹出元素并复制到0处,因此a的值不会发生任何变化。
useBeforeIncrement istore4执行之后
最后返回a,即将0处的值压入栈顶,然后弹出,返回给调用当前方法(useBeforeInc)的方法(main)。
胡一刀main方法,将useBeforeIncrement的返回值存入局部变量表1处(赋值给变量b), 而a仍然是原来的100。
然后iload_1将100压入栈顶,作为IncreseBeforeUse的实际参数。
给b赋值之后main方法栈帧
接下来执行AddA.IncreseBeforeUse(int a)方法,与useBeforeIncrement原理相似,区别只是在于++a的字节码指令不同。
执行结束后,main方法栈桢如图:
end
网友评论