05.局部变量表与操作数栈

1) 概述

  1. JVM的字节码执行引擎,功能基本就是输入字节码文件,然后对字节码进行解析并处理,最后输出执行的结果。
  2. 实现方式可能有通过解释器直接执行字节码,或者适通过及时编译器产生本地代码,也就是编译执行,当让也可能两者皆有。
    • HotSpot就是两者皆有,使用频率较多的代码JIT动态编译为本地代码,频率较少的就解释执行。

2) 栈帧概述

  1. 栈帧是用于执行JVM进行方法调用和方法执行的数据结构。
  2. 栈帧随着方法调用而创建,随着方法结束而销毁。
  3. 栈帧里面存储了方法的 局部变量操作数栈、动态连接、方法返回地址等信息。

栈帧的概念结构.png

3) 局部变量表

  1. 局部变量表:用来存放方法参数和方法内部定义的局部变量的存储空间。
  2. 以slot为单位,目前一个slot存放32位以内的数据类型
  3. 对于64位的数据占2个slot
  4. 对于实例方法,第0位slot存放的是this,然后从1到n,依次分配给参数列表
    • 对于静态方法,则从0位开始依次分配给参数列表
  5. 然后根据方法体内部定义的变量顺序和作用域来分配slot
public class Test1 {
    public int add(int a, int b) {
        int c = a + b;
        return a + b + c;
        /* javap -verbose 得到slot分配情况
         LocalVariableTable:
           Start  Length  Slot  Name   Signature
               0      10     0  this   Lcom/jvm/stack/Test1;
               0      10     1     a   I
               0      10     2     b   I
               4       6     3     c   I
         */
    }
}
  1. slot是复用的,以节省栈帧的空间,这种设计可能会影响到系统的垃圾收集行为。
// -Xms10m -Xmx10m
public static void main(String[] args) {
  {
      byte[] bs1 = new byte[1024 * 1204];
      byte[] bs2 = new byte[1024 * 1204];
      byte[] bs3 = new byte[1024 * 1204];

      /* 此时slot的情况
      0 -- args -- 堆引用
      1 -- bs1  -- 堆引用
      2 -- bs2  -- 堆引用
      3 -- bs3  -- 堆引用
      */

      System.gc();
      printMemory(); // freeMemory :2.98663330078125

      bs1 = null;
      /* 显式将bs1置为null, slot 1则空闲出来*/
  }
  System.gc();
  printMemory(); // freeMemory :4.8661346435546875

  /* 代码块结束,上述的bs1 bs2 bs3变量的作用域已经结束,
  所占用的slot可以被后续定义的变量复用 */

  int a = 5;
  int b = 5;

  /* 此时slot的使用情况
      0 -- args -- 堆引用
      1 -- a    -- I
      2 -- b    -- I
      3 -- bs3  -- 堆引用
   */

  System.gc();
  printMemory(); // freeMemory :6.8662872314453125

  String c = "a";
  /* 此时slot的使用情况
     0 -- args -- 堆引用
     1 -- a    -- I
     2 -- b    -- I
     3 -- c  -- 堆引用
   */

  System.gc();
  printMemory(); // freeMemory :8.866722106933594
}

public static void printMemory() {
  //System.out.println("totalMemory:" + Runtime.getRuntime().totalMemory()/1024.0/1024.0);
  System.out.println("freeMemory :" + Runtime.getRuntime().freeMemory()/1024.0/1024.0);
  //System.out.println("maxMemory  :" + Runtime.getRuntime().maxMemory()/1024.0/1024.0);
}

4) 操作数栈

  1. 操作数栈:用来存放方法运行期间,各个指令操作的数据。
  2. 操作数栈中元素的数据类型必须和字节码指令的顺序严格匹配
    • 数据类型和插槽位置的数据类型必须一一对应:比如指令 iconst_1,那么插槽1位置的类型必须是I
  3. 虚拟机在实现栈帧的时候可能会做一些优化,让两个栈帧出现部分重叠区域,以存放公用的数据
package com.lc.sprnigcloud.stack;

/**
 * @author hahadasheng
 * @since 2020/12/28
 */
public class Test {

    public static void main(String[] args) {
        test();
    }

    /* LocalVariableTable:
    Start  Length  Slot  Name   Signature
        2      51     0     a   I
       16      37     1     b   I
       35      18     2     c   I
     */
    public static void test() {
        int a = 1;
        a = a++;
        /*
         0: iconst_1            -> 常数1
         1: istore_0            -> 将常数1存放在局部变量表slot_0的位置,也就是a
         2: iload_0             -> 将slot_0中存放a的值1 放入栈中
         3: iinc          0, 1  -> slot_0中a的值自增1,1 + 1 = 2
         6: istore_0            -> 将栈中的值1赋值到slot_0的位置,所以此时a的值还是为1
         */
        System.out.println(a); // 1

        int b = 1;
        b = b++ * ++b;
        /*
        14: iconst_1            -> 常数1
        15: istore_1            -> 将1放在slot_1的位置
        16: iload_1             -> 将slot_1的值1入栈 栈:[1]
        17: iinc          1, 1  -> slot_1位置的1自增1,1+1=2
        20: iinc          1, 1  -> slot_1位置的2自增1,2+1=3
        23: iload_1             -> 将slot_1的值3入栈 栈:[3,1]
        24: imul                -> 将栈中的两个数相乘 3 * 1 = 3,栈中的值为3
        25: istore_1            -> 将栈中值3放在slot_1的位置
         */
        System.out.println(b); // 3

        int c = 1;
        c = ++c * c++;
        /*
        33: iconst_1            -> 常数1
        34: istore_2            -> 将常数1放在局部变量表slot_2的位置
        35: iinc          2, 1  -> slot_2位置数自增1,1+1=2
        38: iload_2             -> 将slot_2位置的2入栈 栈:[2]
        39: iload_2             -> 将slot_2位置的2入栈 栈:[2,2]
        40: iinc          2, 1  -> 将slot_2位置的2自增1,2+1=3
        43: imul                -> 将栈中的值取出来进行乘法操作 2*2=4,栈中的值变为4
        44: istore_2            -> 将栈中值4存放在slot_2的位置
         */
        System.out.println(c); // 4
    }
}

5) 动态连接

  1. 动态连接:每个栈帧持有一个指向运行时常量池中该栈帧所属方法的引用,以支持方法调用过程中的动态连接。
  2. 动态连接分类:
    1. 静态解析:类加载的时候,符号引用就转化成直接引用
    2. 动态连接:运行期间转换为直接引用(动态分派)

6) 方法返回地址

  1. 方法返回地址:方法执行后返回的地址,
    • 无论正常退出还是异常退出,都得返回到方法被调用的位置,程序才能继续执行

7) 方法调用

  1. 方法调用:方法调用就是确定具体调用哪一个方法,并不涉及方法内部的执行过程。
    • 不一定要调用这个方法
  2. 部分方法是直接在类加载的解析阶段,就确定了直接引用关系
    • 静态方法、私有方法、实例构造器、父类方法
  3. 但是对于实例方法,也称虚方法,因为重载和多态,需要运行期间动态委派
    • 例如虚拟机调用此方法的指令为 invokevirtual

8) 静态分派和动态分派

  1. 分派:分为静态分派和动态分派

  2. 静态分派:所有依赖静态类型来定位方法执行版本的分派方式

    • 比如:重载方法(依据传参确定)
  3. 动态分派:根据运行期间的实际类型来定位方法执行版本的分派方式,

    • 比如:重写覆盖方法、实现的接口等
  4. 单分派和多分派:就是按照分派思考的纬度,多于一个的就算多分派,只有一个的称为单分派

    • 只有一个确认的,没有重载、重写、多态等可能有多个可能的就是单分派
  5. 如何执行方法中的字节码指令:JVM通过基于栈的字节码解释器引擎来执行指令,JVM的指令集也是基于栈的。

你可能感兴趣的:(05.局部变量表与操作数栈)