剑指JVM:虚拟机实践与性能调优
上QQ阅读APP看书,第一时间看更新

4.2 栈的存储单位

每个线程都有自己的栈,栈中的数据都是以栈帧(Stack Frame)的形式存在。在这个线程上正在执行的每个方法都各自对应一个栈帧,也就是说栈帧是Java中方法的执行环境。栈帧是一个内存区块,是一个数据集,维系着方法执行过程中的各种数据信息。

在一条活动线程中,一个时间点上只会有一个活动的栈帧,即只有当前正在执行的方法的栈帧(栈顶栈帧)是有效的,这个栈帧被称为当前栈帧,与当前栈帧相对应的方法就是当前方法,定义这个方法的类就是当前类。如图4-8所示,执行引擎运行的所有字节码指令只针对当前栈帧进行操作。如果在该方法中调用了其他方法,对应的新的栈帧会被创建出来,放在栈的顶端,成为新的当前帧。JVM直接对Java栈的操作只有两个,就是对栈帧的压栈和出栈,遵循“先进后出”“后进先出”原则。

图4-8 栈中的当前栈帧

代码清单4-4演示了在一条活动线程中,一个时间点上只会有一个活动的栈帧。可以看到在main()方法中调用了methodA(),methodA()中调用完methodB()之后又输出了一条语句。

代码清单4-4 CurrentFrameTest.java

输出结果如图4-9所示,可以看到先执行了methodA()中的第一条输出语句,接着又执行了methodB(),之后又返回执行了methodA()中的第二条输出语句,证明了当执行某一个方法的时候其他方法是没有在执行的,即一个时间点上,只会有一个活动的栈帧。

图4-9 栈中栈帧的执行顺序

代码清单4-5演示了栈帧遵循“先进后出”“后进先出”原则。代码中method1()调用了method2(),method2()调用了method3()。

代码清单4-5 “先进后出”“后进先出”原则

输出结果如图4-10所示,当一个方法method1()被调用时就产生了一个栈帧F1,并被压入到栈中,method1()方法又调用了method2()方法,于是产生栈帧F2也被压入栈,method2()方法又调用了method3()方法,于是产生栈帧F3也被压入栈,执行完毕后,先弹出F3栈帧,再弹出F2栈帧,最后弹出F1栈帧。以上案例证明了栈帧遵循“先进后出”“后进先出”原则。

不同线程中所包含的栈帧是不允许存在相互引用的,即不可能在一个栈帧之中引用另外一个线程的栈帧。如果当前方法调用了其他方法,方法返回之际,当前栈帧会传回此方法的执行结果给前一个栈帧,接着虚拟机会丢弃当前栈帧,使得前一个栈帧重新成为当前栈帧。Java方法有两种返回方法的方式:一种是正常的函数返回,使用return指令;另外一种是抛出异常。不管使用哪种方式,都会导致栈帧被弹出。

图4-10 栈中嵌套方法执行的顺序

下面介绍每个栈帧中存储的内容,如图4-11所示。

· 局部变量表(Local Variables)。

· 操作数栈(Operand Stack)(或表达式栈)。

· 动态链接(Dynamic Linking)(或指向运行时常量池的方法引用)。

· 方法返回地址(Return Address)(或方法正常退出或异常退出的定义)。

· 一些附加信息。例如,对程序调试提供支持的信息。

图4-11 栈帧的内部结构

在多线程环境中,当前线程和当前栈帧如图4-12所示。

图4-12 多线程的栈和栈帧

下面的小节分别介绍栈帧中存储的内容。