JVM 栈帧

JVM 栈帧

 一、栈帧

栈帧(Frame)是用来存储数据和部分过程结果的数据结构,同时也被用来处理动态链接(Dynamic Linking)、方法返回值和异常分派(Dispatch Exception)。

栈帧随着方法调用而创建,随着方法结束而销毁——无论方法是正常完成还是异常完成(抛出了在方法内未被捕获的异常)都算作方法结束。栈帧的存储空间分配在Java虚拟机栈之中,每一个栈帧都有自己的局部变量表、操作数栈和指向当前方法所属的类的运行时常量池的引用。

局部变量表和操作数栈的容量是在编译期确定,并通过方法的Code属性保存及提供给栈帧使用。因此,栈帧容量的大小仅仅取决于Java虚拟机的实现和方法调用时可被分配的内存。

在一条线程之中,只有目前正在执行的那个方法的栈帧是活动的。这个栈帧就被称为是当前栈帧(Current Frame),这个栈帧对应的方法就被称为是当前方法(Current Method),定义这个方法的类就称作当前类(Current Class)。对局部变量表和操作数栈的各种操作,通常都指的是对当前栈帧的对局部变量表和操作数栈进行的操作。

如果当前方法调用了其他方法,或者当前方法执行结束,那这个方法的栈帧就不再是当前栈帧了。当一个新的方法被调用,一个新的栈帧也会随之而创建,并且随着程序控制权移交到新的方法而成为新的当前栈帧。当方法返回的之际,当前栈帧会传回此方法的执行结果给前一个栈帧,在方法返回之后,当前栈帧就随之被丢弃,前一个栈帧就重新成为当前栈帧了。

栈帧是线程本地私有的数据,不可能在一个栈帧之中引用另外一条线程的栈帧。

二、局部变量表

每个栈帧内部都包含一组称为局部变量表(Local Variables)的变量列表。栈帧中局部变量表的长度由编译期决定,并且存储于类和接口的二进制表示之中,既通过方法的Code属性保存及提供给栈帧使用。

一个局部变量(Slot)可以保存一个类型为boolean、byte、char、short、float、reference和returnAddress的数据,两个局部变量可以保存一个类型为long和double的数据。

局部变量使用索引来进行定位访问,第一个局部变量的索引值为零,局部变量的索引值是从零至小于局部变量表最大容量的所有整数。

long和double类型的数据占用两个连续的局部变量,这两种类型的数据值采用两个局部变量之中较小的索引值来定位。例如我们讲一个double类型的值存储在索引值为n的局部变量中,实际上的意思是索引值为n和n+1的两个局部变量都用来存储这个值。索引值为n+1的局部变量是无法直接读取的,但是可能会被写入,不过如果进行了这种操作,就将会导致局部变量n的内容失效掉。

上文中提及的局部变量n的n值并不要求一定是偶数,Java虚拟机也不要求double和long类型数据采用64位对齐的方式存放在连续的局部变量中。虚拟机实现者可以自由地选择适当的方式,通过两个局部变量来存储一个double或long类型的值。

Java虚拟机使用局部变量表来完成方法调用时的参数传递,当一个方法被调用的时候,它的参数将会传递至从0开始的连续的局部变量表位置上。特别地,当一个实例方法被调用的时候,第0个局部变量一定是用来存储被调用的实例方法所在的对象的引用(即Java语言中的“this”关键字)。后续的其他参数将会传递至从1开始的连续的局部变量表位置上。

三、操作数栈

每一个栈帧内部都包含一个称为操作数栈(Operand Stack)的后进先出(Last-In-First-Out,LIFO)栈。栈帧中操作数栈的长度由编译期决定,并且存储于类和接口的二进制表示之中,既通过方法的Code属性保存及提供给栈帧使用。

在上下文明确,不会产生误解的前提下,我们经常把“当前栈帧的操作数栈”直接简称为“操作数栈”。

操作数栈所属的栈帧在刚刚被创建的时候,操作数栈是空的。Java虚拟机提供一些字节码指令来从局部变量表或者对象实例的字段中复制常量或变量值到操作数栈中,也提供了一些指令用于从操作数栈取走数据、操作数据和把操作结果重新入栈。在方法调用的时候,操作数栈也用来准备调用方法的参数以及接收方法返回结果。

举个例子,iadd字节码指令的作用是将两个int类型的数值相加,它要求在执行的之前操作数栈的栈顶已经存在两个由前面其他指令放入的int型数值。在iadd指令执行时,2个int值从操作栈中出栈,相加求和,然后将求和结果重新入栈。在操作数栈中,一项运算常由多个子运算(Subcomputations)嵌套进行,一个子运算过程的结果可以被其他外围运算所使用。

每一个操作数栈的成员(Entry)可以保存一个Java虚拟机中定义的任意数据类型的值,包括long和double类型。

在操作数栈中的数据必须被正确地操作,这里正确操作是指对操作数栈的操作必须与操作数栈栈顶的数据类型相匹配,例如不可以入栈两个int类型的数据,然后当作long类型去操作他们,或者入栈两个float类型的数据,然后使用iadd指令去对它们进行求和。有一小部分Java虚拟机指令(例如dup和swap指令)可以不关注操作数的具体数据类型,把所有在运行时数据区中的数据当作裸类型(Raw Type)数据来操作,这些指令不可以用来修改数据,也不可以拆散那些原本不可拆分的数据,这些操作的正确性将会通过Class文件的校验过程来强制保障。

在任意时刻,操作数栈都会有一个确定的栈深度,一个long或者double类型的数据会占用两个单位的栈深度,其他数据类型则会占用一个单位深度。

分析:

有如下代码:

package cc.lixiaohui.demo;  
public class Foo {  
    public static void main(String[] args) {  
        int a = 1;  
        int b = 2;  
        int c = a + b;  
    }  
}  

 利用javap工具生成虚拟机汇编代码(java虚拟机指令集):

 括号内的注释我是自己加的,其余是javap生成的

E:\EclipseWorkspace\demo-foo\target\classes\cc\lixiaohui\demo>javap -c -l Foo.class  
Compiled from "Foo.java"  
public class cc.lixiaohui.demo.Foo {  
  public cc.lixiaohui.demo.Foo();   (//这是默认构造方法)  
    Code:  
       0: aload_0  
       1: invokespecial #8                  // Method java/lang/Object."<init>":()V  
       4: return  
    LineNumberTable:  
      line 3: 0  
    LocalVariableTable:  
      Start  Length  Slot  Name   Signature  
             0       5     0  this   Lcc/lixiaohui/demo/Foo;  
  
  public static void main(java.lang.String[]);  
    Code:  
       0: iconst_1     (//把int值1压入操作数栈)  
       1: istore_1     (//把栈顶值存储到局部变量表下标为1的位置)  
       2: iconst_2     (//把int值2压入操作数栈)  
       3: istore_2     (//把栈顶值存储到局部变量表下标为2的位置)  
       4: iload_1      (//取局部变量表中下标为1的变量压栈)  
       5: iload_2      (//取局部变量表中下标为2的变量压栈)  
       6: iadd         (//从操作数栈中弹出两个int值进行相加操作,相加的结果压栈)  
       7: istore_3     (//把栈顶值存储到局部变量表下标为3的位置)  
       8: return  
    LineNumberTable: (//这是java代码行与该栈帧指令代码行的映射)  
      line 5: 0        (//java代码第五行为“int a = 1”,对应的虚拟机汇编代码为“iconst_1”)  
      line 6: 2  
      line 7: 4  
      line 8: 8  
    LocalVariableTable:  
      Start  Length  Slot  Name   Signature  
             0       9     0  args   [Ljava/lang/String;  
             2       7     1     a   I  
             4       5     2     b   I  
             8       1     3     c   I  
}  

 可以看到执行过程中不断在局部变量表和操作数栈间来回传递数据。