JVM运行时数据区之——虚拟机栈

虚拟机栈简介

虚拟机栈是JVM运行时数据区的一个重要部分,他是线程私有的,同时可能存在StackOverFlowErrorOutOfMemoryErrorStackOverFlowError出现在栈帧花费完栈内存容量时发生,OutOfMemoryError出现在动态扩展栈容量时无内存供其扩展时发生。 我们可以在运行时指定 -Xss 容量指定本次JVM中的栈容量大小。

栈帧简介

对于栈中的内容,都为一个一个的栈帧,栈帧随着方法的调用而创建或销毁,每一个栈帧都是用于描述运行中的方法的一些状态数据。通常认为栈帧中存储的内容主要分为五个部分:

  • LocalVariableTable 局部变量表
  • Operand stack 操作数栈
  • Dynamic Linking 动态链接
  • Return Adress 方法返回地址
  • 其他数据区

LocalVariableTable

局部变量表代表着本次方法中需要用到的局部变量,其操作时存入的位置便是我们的局部方法表。 对于局部方法表,有以下几个特点:

  1. 局部变量表的大小在编译期间就已经确定了
  2. 局部变量表槽位大小通常为32位,因此double long需要占用两个槽
  3. 构造函数或对象方法中存在隐含的this局部变量
  4. 局部变量的槽可复用

我们可以看以下示例:

public class Stack_LocalVariableTable {
    public Stack_LocalVariableTable(int a){
        double b = 10;
        {
            char c = 'a';
            c++;
        }
        int d = 20;
    }
}

他的字节码文件中关于局部变量表的解析如下:

LocalVariableTable:
Start  Length  Slot  Name   Signature
   12       7     4     c   C
    0      24     0  this   Lcom/linmu/Stack_LocalVariableTable;
    0      24     1     a   I
    8      16     2     b   D
   23       1     4     d   I

其中Start代表该局部变量作用域起始PC位置, Length代表作用域持续PC数量, Slot代表槽位索引, Name代表变量名,Signature代表类型。

我们可以看到符合我们上面的几个特点:

  1. 存在隐含的this局部变量
  2. c的槽位被后来的d重复使用
  3. double数据的局部变量占用槽位为2

Operand stack

JVM中的操作数栈是用类数组结构实现的,当我们需要对数据进行操作时,都是将数据存入操作数栈再进一步进行操作。 和局部变量表一样,操作数栈大小也是在编译期间就被确定了,我们来看一个示例:

public static int sum(int a, int b){
    int c = a + b;
    return c;
}
public static int sum(int, int);
descriptor: (II)I
flags: ACC_PUBLIC, ACC_STATIC
Code:
  stack=2, locals=3, args_size=2
     0: iload_0
     1: iload_1
     2: iadd
     3: istore_2
     4: iload_2
     5: ireturn
  LineNumberTable:
    line 33: 0
    line 34: 4
  LocalVariableTable:
    Start  Length  Slot  Name   Signature
        0       6     0     a   I
        0       6     1     b   I
        4       2     2     c   I

可以看到,操作数栈大小为2, 并且简单的查阅字节码指令我们也能发现,方法首先将参数(槽位0和1的形参值)load到操作数栈中,然后使用add将操作数栈中的两个操作数相加,并且将其store:取出并保存在槽位为2的局部变量表中。 随后再将槽位为2的变量值取出,并且返回。

Dynamic Linking

在jdk1.7前,字节码文件中有关方法调用的指令只存在以下几种:

  • invokestatic
  • invokespecial
  • invokevirtual
  • invokeinterface

在jdk1.7时引入了一个新的方法指令:

  • invokedynamic

这个指令在jdk1.8的lambda表达式出现后才会自然的出现在我们编译后的字节码文件中。

简单介绍完方法指令,我们接下来说说java中虚函数的含义,学习过c++的同学都知道, c++中期望实现方法调用的多态时,需要声明方法为虚方法,并且子类重写虚方法才能实现。 我们这里的虚方法与其有相似之处也有不同之处:

在java中,如果无法在编译期确定的方法即为虚方法,例如对于static方法、本类方法、指定父类方法等等都是非虚方法。 而那些有多态性质的方法调用: 例如接口、抽象类的对象的方法调用便为我们的虚方法。

虚拟机栈中,对于方法调用需要存在一个动态链接,他决定了我们的虚方法具体需要指向的方法。如以下示例:

public class Stack_DynamicLinking {
    interface Person{
        void talk();
        void eat();
        void sleep();
    }
    
    class Father implements Person{
        @Override
        public void talk() {}
        @Override
        public void eat() {}
        @Override
        public void sleep() {}
       
        public void laugh(){}
    }
    
    class Son extends Father{
        @Override
        public void laugh() {}
    }
    
    public static void main(String[] args) {
        Person person = new Stack_DynamicLinking().new Son();
        person.eat(); //虚方法
        
        Father father = (Father) person;
        father.laugh(); //虚方法
        
        Son son = (Son) father;
        son.laugh();  //非虚方法
    }
}
public static void main(java.lang.String[]);
    descriptor: ([Ljava/lang/String;)V
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=4, locals=4, args_size=1
         0: new           #2                  // class com/linmu/Stack_DynamicLinking$Son
         3: dup
         4: new           #3                  // class com/linmu/Stack_DynamicLinking
         7: dup
         8: invokespecial #4                  // Method "<init>":()V
        11: dup
        12: invokevirtual #5                  // Method java/lang/Object.getClass:()Ljava/lang/Class;
        15: pop
        16: invokespecial #6                  // Method com/linmu/Stack_DynamicLinking$Son."<init>":(Lcom/linmu/Stack_DynamicLinking;)V
        19: astore_1
        20: aload_1
        21: invokeinterface #7,  1            // InterfaceMethod com/linmu/Stack_DynamicLinking$Person.eat:()V
        26: aload_1
        27: checkcast     #8                  // class com/linmu/Stack_DynamicLinking$Father
        30: astore_2
        31: aload_2
        32: invokevirtual #9                  // Method com/linmu/Stack_DynamicLinking$Father.laugh:()V
        35: aload_2
        36: checkcast     #2                  // class com/linmu/Stack_DynamicLinking$Son
        39: astore_3
        40: aload_3
        41: invokevirtual #10                 // Method com/linmu/Stack_DynamicLinking$Son.laugh:()V
        44: return
      LineNumberTable:
        line 37: 0
        line 38: 20
        line 40: 26
        line 41: 31
        line 43: 35
        line 44: 40
        line 45: 44
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0      45     0  args   [Ljava/lang/String;
           20      25     1 person   Lcom/linmu/Stack_DynamicLinking$Person;
           31      14     2 father   Lcom/linmu/Stack_DynamicLinking$Father;
           40       5     3   son   Lcom/linmu/Stack_DynamicLinking$Son;
}

Return Adress

方法返回地址是最为简单的一个区域, 在每次方法结束后,栈帧都会被弹出,然后进程回到上一个方法的执行中。 返回地址即是标识我们的代码应该从哪儿继续执行。
也就是说,他存储的为PC寄存器中的数据,标识当前方法返回后原方法的执行位置。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值