JVM-虚拟机栈

254 阅读8分钟

前言

JVM内存模型中,程序计数器和虚拟机是线程私有的,程序计数器保存了线程执行到的下一条指令的地址,虚拟机保存了什么,我们来看看

虚拟机栈

image.png

我们先从一个整体的视角看JVM的虚拟机栈

  1. 方法是Java虚拟机中最基本的执行单位,以栈帧的格式作为栈元素存在于虚拟机栈中

  2. 每个栈帧都包括了局部变量表、操作数栈、方法返回地址、动态链接和一些额外的附加信息

  3. 同一时刻、同一个线程里面,只有位于栈顶的栈帧是有效的,被称为当前栈帧,与这个栈帧关联的方法被称为当前方法

局部变量表

局部变量表是一组变量值的存储空间,用于存放方法参数和方法内部定义的局部变量,局部变量表的大小是在编译期就确定下来的

slot

先看一段代码,感受一下局部变量表存储的东西

public class LocalVariablesTableTest {
​
    public static void main(String[] args) {
        LocalVariablesTableTest localVariablesTableTest = new LocalVariablesTableTest();
        localVariablesTableTest.methodA();
    }
    // 这个方法中定义了不同类型的变量
    public void methodA() {
        byte by = 127;
        char c = 'a';
        boolean b = false;
        short s = 15;
        int i = 10;
        long l = 100L;
        float f = 10;
        double d = 20.2;
        String str = "str";
    }
}

局部变量表最大槽数在编译期就确定了

image.png

image.png

从上面的实验中,我们可以得出一下结论

  1. 局部变量表是以变量槽(Variable Slot) 为基本单位,一个变量槽可以存放一个32位以内的数据类型,Java中占用不超过32位存储空间的数据类型有boolean、byte、char、short、int、float、reference(一个对象实例的引用),对于64位的数据类型,JVM会以高位对齐的方式为其分配两个连续的变量槽空间,java中明确的64位的数据类型只有long和double
  2. JVM通过索引定位的方式访问局部变量表,如果是32位以内的数据类型,索引N表示变量槽N,如果是64位的数据类型,会使用前一个索引
  3. 实例方法中,局部变量表的第0位变量槽默认是所属对象实例的引用,也就是this
重复利用的slot
public class LocalVariablesTableTest {
​
    public static void main(String[] args) {
        LocalVariablesTableTest localVariablesTableTest = new LocalVariablesTableTest();
        localVariablesTableTest.methodA();
    }
​
    public void methodB() {
        int a = 10;
        {
            int b = 20;
        }
        int c = 30;
    }
}

image.png

通过上图可以看到b变量跟c变量使用了同一个slot

操作数栈

操作数栈的作用有点类似于寄存器,用来存储临时数据,它有一下特点

  1. 32位数据类型所占的栈容量为1,64位数据类型所占的栈容量为2
  2. 操作数栈的最大深度在编译期的时候已经确定
public class OperatorTest {
​
    public int add() {
        int i = 10;
        int j = 20;
        return i + j;
    }
​
}

对应字节码

0 bipush 10
2 istore_1
3 bipush 20
5 istore_2
6 iload_1
7 iload_2
8 iadd
9 ireturn

根据这些字节码,局部变量表、操作数栈、程序计数器之间的配合执行如下

image.png

  1. 程序计数器存储了下一条执行指令的地址
  2. 局部变量表存储了方法参数和方法内部定义的局部变量
  3. 操作数栈存储了临时数据
动态链接

了解动态链接之前,先要了解什么是运行时常量池,运行时常量池是方法区的一部分。Class文件中除了有类的版本、字段、方法、接口等描述信息外,还有一项信息是常量池表(Constant Pool Table),用于存放编译期生成的各种字面量与符号引用,这部分内容将在类加载后存放到方法区的运行时常量池中。

看下面一段代码

public class DynamicLinkingTest {
​
    public static void main(String[] args) {
        DynamicLinkingTest dynamicLinkingTest = new DynamicLinkingTest();
        dynamicLinkingTest.methodA();
    }
​
    public void methodA() {
        int a = 10;
        int b = 20;
        int c = a + b;
    }
}

对应字节码

  jvm javap -v DynamicLinkingTest.class
Classfile /Users/zhangxiaobin/IdeaProjects/jvm-demo/target/classes/com/example/jvm/DynamicLinkingTest.class
  Last modified 2021-8-4; size 630 bytes
  MD5 checksum bd9d215eb905d941f1241febea17a3fc
  Compiled from "DynamicLinkingTest.java"
public class com.example.jvm.DynamicLinkingTest
  minor version: 0
  major version: 52
  flags: ACC_PUBLIC, ACC_SUPER

# 这里就是运行时常量区
Constant pool:
   #1 = Methodref          #5.#25         // java/lang/Object."<init>":()V
   #2 = Class              #26            // com/example/jvm/DynamicLinkingTest
   #3 = Methodref          #2.#25         // com/example/jvm/DynamicLinkingTest."<init>":()V
   #4 = Methodref          #2.#27         // com/example/jvm/DynamicLinkingTest.methodA:()V
   #5 = Class              #28            // java/lang/Object
   #6 = Utf8               <init>
   #7 = Utf8               ()V
   #8 = Utf8               Code
   #9 = Utf8               LineNumberTable
  #10 = Utf8               LocalVariableTable
  #11 = Utf8               this
  #12 = Utf8               Lcom/example/jvm/DynamicLinkingTest;
  #13 = Utf8               main
  #14 = Utf8               ([Ljava/lang/String;)V
  #15 = Utf8               args
  #16 = Utf8               [Ljava/lang/String;
  #17 = Utf8               dynamicLinkingTest
  #18 = Utf8               methodA
  #19 = Utf8               a
  #20 = Utf8               I
  #21 = Utf8               b
  #22 = Utf8               c
  #23 = Utf8               SourceFile
  #24 = Utf8               DynamicLinkingTest.java
  #25 = NameAndType        #6:#7          // "<init>":()V
  #26 = Utf8               com/example/jvm/DynamicLinkingTest
  #27 = NameAndType        #18:#7         // methodA:()V
  #28 = Utf8               java/lang/Object
{
  public com.example.jvm.DynamicLinkingTest();
    descriptor: ()V
    flags: ACC_PUBLIC
    Code:
      stack=1, locals=1, args_size=1
         0: aload_0
         1: invokespecial #1                  // Method java/lang/Object."<init>":()V
         4: return
      LineNumberTable:
        line 3: 0
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0       5     0  this   Lcom/example/jvm/DynamicLinkingTest;

  public static void main(java.lang.String[]);
    descriptor: ([Ljava/lang/String;)V
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=2, locals=2, args_size=1
         0: new           #2                  // class com/example/jvm/DynamicLinkingTest
         3: dup
         4: invokespecial #3                  // Method "<init>":()V
         7: astore_1
         8: aload_1
         9: invokevirtual #4                  // Method methodA:()V
        12: return
      LineNumberTable:
        line 6: 0
        line 7: 8
        line 8: 12
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0      13     0  args   [Ljava/lang/String;
            8       5     1 dynamicLinkingTest   Lcom/example/jvm/DynamicLinkingTest;

  public void methodA();
    descriptor: ()V
    flags: ACC_PUBLIC
    Code:
      stack=2, locals=4, args_size=1
         0: bipush        10
         2: istore_1
         3: bipush        20
         5: istore_2
         6: iload_1
         7: iload_2
         8: iadd
         9: istore_3
        10: return
      LineNumberTable:
        line 11: 0
        line 12: 3
        line 13: 6
        line 14: 10
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0      11     0  this   Lcom/example/jvm/DynamicLinkingTest;
            3       8     1     a   I
            6       5     2     b   I
           10       1     3     c   I
}
SourceFile: "DynamicLinkingTest.java"

在字节码中,main方法通过invokevirtual调用了methodA,调用的过程是这样的

  1. invokevirtual旁边有#4,在Constant pool找到#4,#4指向了#2.#27
  2. 在Constant pool中继续寻找,#2指向了#26,#27指向了#18:#7
  3. 在Constant pool中继续寻找,#26指向了com/example/jvm/DynamicLinkingTest,#18:#7 分别是methodA和()V(void)

动态链接

  1. 每个栈帧都包含一个指向运行时常量池中该栈帧所属方法的引用,持有这个引用是为了支持方法调用过程中的动态链接
  2. 如上,每个Class文件中都包含了大量的符号引用,动态链接会将这些符号引用转换为直接引用
为什么要使用运行时常量池

不同的方法可能调用相同的常量或者方法,使用运行时常量池可以只保存一份数据,节省资源

方法返回地址

一个方法在执行后,有两种方式退出这个方法

  1. 正常返回,执行引擎遇到任意一个方法返回的字节码指令
  2. 异常返回,方法执行过程中遇到了异常,并且这个异常没有在方法体内得到妥善处理
正常退出时

字节码指令中会包含以下的某个指令,主调方法的PC计数器的值就可以作为返回地址,栈帧中很可能会保存这个计数器值

  • ireturn:当返回值是boolean,byte,char,short和int类型时使用
  • lreturn:Long类型
  • freturn:Float类型
  • dreturn:Double类型
  • areturn:引用类型
  • return:返回值类型为void的方法、实例初始化方法、类和接口的初始化方法
异常退出时

先看一段代码,再看对应的字节码,感受一下处理的方式

public class ErrorReturnTest {
​
    public static void main(String[] args) {
​
        ErrorReturnTest errorReturnTest = new ErrorReturnTest();
        try {
            errorReturnTest.methodA();
        } catch (Exception e) {
            System.out.println("error");
        }
    }
    public void methodA() {
​
        int i = 1;
        int j = 0;
        int k = i / j;
​
    }
}

对应字节码

➜  jvm javap -v ErrorReturnTest.class
​
.......
​
  public static void main(java.lang.String[]);
    descriptor: ([Ljava/lang/String;)V
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=2, locals=3, args_size=1
         0: new           #2                  // class com/example/jvm/ErrorReturnTest
         3: dup
         4: invokespecial #3                  // Method "<init>":()V
         7: astore_1
         8: aload_1
         9: invokevirtual #4                  // Method methodA:()V
        12: goto          24
        15: astore_2
        16: getstatic     #6                  // Field java/lang/System.out:Ljava/io/PrintStream;
        19: ldc           #7                  // String error
        21: invokevirtual #8                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
        24: return
      Exception table:
         from    to  target type
             8    12    15   Class java/lang/Exception
      LineNumberTable:
        line 7: 0
        line 9: 8
        line 12: 12
        line 10: 15
        line 11: 16
        line 14: 24
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
           16       8     2     e   Ljava/lang/Exception;
            0      25     0  args   [Ljava/lang/String;
            8      17     1 errorReturnTest   Lcom/example/jvm/ErrorReturnTest;
      StackMapTable: number_of_entries = 2
        frame_type = 255 /* full_frame */
          offset_delta = 15
          locals = [ class "[Ljava/lang/String;", class com/example/jvm/ErrorReturnTest ]
          stack = [ class java/lang/Exception ]
        frame_type = 8 /* same */
​
......
​
SourceFile: "ErrorReturnTest.java"

可以在字节码中看到

Exception table:
         from    to  target type
             8    12    15   Class java/lang/Exception

表示8到12行的字节码指令,跳转到15行进行处理,处理的类型是java/lang/Exception

一些附加信息

某些虚拟机实现会增加一些规范里没有描述的信息到栈帧之中,例如与调试、 性能收集相关的信息

参考资料

《深入理解java虚拟机》

尚硅谷视频