在讨论概念时,一般会把动态连接、方法返回地址与其他附加信息全部归为一类,称为栈帧信息

局部变量表

局部变量的作用域是从其被赋值的下一行指令开始生效。

  1. package com.dragon.jvmstack.stackframe;
  2. public class LocalVariableTest {
  3. public void test() {
  4. int num;
  5. String name = "233";
  6. String temp;
  7. String[] names = new String[10];
  8. num = getValue();
  9. }
  10. public int getValue() {
  11. int a = 3;
  12. a++;
  13. return a;
  14. }
  15. }

编译后:
image.png
image.png
image.png
image.png

  • 图 1 是 test 方法的 code,图 2 可以看到 test 方法字节指令一共16行(0-15);
  • 从图 4 可看到,String 类型的 temp 并没有出现在局部变量表中,而且变量索引少了个 3,实际上这是 temp 只声明没有被赋值,就不会分配空间,也就不会添加到局部变量表中
  • 从图 4 我们又发现局部变量表还有一个 this 变量,类型是我们的 test 方法所属类,这很容易理解,在代码第 9 行,我们调用了 getValue 方法,将返回值赋给 num,调用方法往往 this 会被我们省略,但实际上是通过 this.getValue() 形式来调用方法的;如果我们在代码中赋值了一个 实例变量,也会把 this 引入到局部变量表中;

局部变量表存放了编译期可知的各种Java虚拟机基本数据类型(boolean、byte、char、short、int、float、long、double)、对象引用(reference 类型,它并不等同于对象本身,可能是一个指向对象起始地址的引用指针,也可能是指向一个代表对象的句柄或者其他与此对象相关的位置)和 returnAddress 类型(指向了一条字节码指令的地址)。

Slot

这些数据类型在局部变量表中的存储空间以局部变量槽(Slot)来表示,其中64位长度的long和double类型的数据会占用两个变量槽,其余的数据类型只占用一个

  • byte、short、char、float在存储前被转换为int,boolean 也被转换为 int,0表示false,非 0 表示true;

局部变量表所需的内存空间在编译期间完成分配,当进入一个方法时,这个方法需要在栈帧中分配多大的局部变量空间是完全确定的,在方法运行期间不会改变局部变量表的大小。请读者注意,这里说的「大小」是指变量槽的数量,虚拟机真正使用多大的内存空间(譬如按照1个变量槽占用 32 个比特、64 个比特,或者更多)来实现一个变量槽,这是完全由具体的虚拟机实现自行决定的事情。

下面的总结可以通过上面的举例来一一验证:

  • JVM 会为局部变量表中的每一个 Slot 都分配一个访问索引,通过这个索引即可成功访问到局部变量表中指定的局部变量值;
  • 当一个实例方法被调用的时候,它的方法参数和方法体内部定义的局部变量将会按照声明顺序被复制到局部变量表中的每一个 Slot 上;
  • 如果需要访问局部变量表中一个 64 bit 的局部变量值时,只需要使用前一个索引即可(64 位占用两个索引 index);
  • 如果当前帧是由构造方法或者实例方法创建的(意思是当前帧所对应的方法是构造器方法或者是普通的实例方法),那么该对象引用 this 将会存放在 index 为 0 的 slot 处,其余的参数按照参数表顺序排列
  • 静态方法中不能引用 this,是因为静态方法所对应的栈帧当中的局部变量表中不存在 this

    Slot的重复利用

    栈帧中的局部变量表中的槽位是可以重复利用的,如果一个局部变量过了其作用域,那么在其作用域之后声明的局部变量就很可能会服用过期的局部变量的槽位,从而达到节省资源的目的。

    1. public class SlotTest {
    2. public void localVal(){
    3. int a=0;
    4. {
    5. int b=a+1;
    6. a = 3;
    7. }
    8. int c=a+1;//c会复用b的槽位
    9. System.out.println(c);
    10. }
    11. }

    image.png
    如果b为以实例对象,则类型引用无法在此表中直接看到,但可以看到其从其locals=4看出其有四个局部变量。

    变量的分类

  • 按照数据类型分:

    • ①基本数据类型;
    • ②引用数据类型;
  • 按照在类中声明的位置分:

    • ①成员变量:在使用前,都经历过默认初始化赋值
      • static修饰:类变量(静态变量):类加载链接的准备 preparation 阶段给类变量默认赋零值——>初始化阶段 initialization 给类变量显式赋值即静态代码块赋值;
      • 不被static修饰:实例变量(全局变量):随着对象的创建,会在堆空间分配实例变量空间,并进行默认赋值
    • ②局部变量:在使用前,必须要进行显式赋值的!否则,编译不通过

      操作数栈

      以数组方式实现的栈,并不可以通过索引操作数据,只能通过入栈、出栈。

      动态链接

      符号引用:#7
      直接引用:

      方法调用

      虚方法与非虚方法

      子类对象的多态性使用前提:
      ①类的继承关系(父类的声明
      ②方法的重写(子类的实现)
  • 非虚方法(与多态性对立的就是非虚方法)

    • 如果方法在编译器就确定了具体的调用版本,这个版本在运行时是不可变的。这样的方法称为非虚方法
    • 静态方法、私有方法、final方法、实例构造器、父类方法(子类通过 super 调用父类方法也是可以确定的)都是非虚方法
  • 虚方法:其他方法称为虚方法

    调用指令

  • 普通调用指令:

  1. invokestatic:调用静态方法,解析阶段确定唯一方法版本;
  2. invokespecial:调用 方法、私有及父类方法,解析阶段确定唯一方法版本;
  3. invokevirtual:调用所有虚方法;
  4. invokeinterface:调用接口方法。
  • 动态调用指令(Java7新增):
  1. invokedynamic:动态解析出需要调用的方法,然后执行 。

前四条指令固化在虚拟机内部,方法的调用执行不可人为干预,而invokedynamic指令则支持由用户确定方法版本。

  • invokestatic 指令和 invokespecial 指令调用的方法称为非虚方法
  • invokevirtual(final 修饰的除外,JVM会把final方法调用也归为invokevirtual指令)指令调用的方法称称为虚方法
  • invokeinterface指令调用的方法称称为虚方法。

    方法返回地址

    方法退出的过程实际上等同于把当前栈帧出栈,因此退出时可能执行的操作有:恢复上层方法的局部变量表和操作数栈,把返回值(如果有的话)压入调用者栈帧的操作数栈中,调整PC计数器的值以指向方法调用指令后面的一条指令等。

    附加信息

    《Java虚拟机规范》允许虚拟机实现增加一些规范里没有描述的信息到栈帧之中,例如与调试、性能收集相关的信息,这部分信息完全取决于具体的虚拟机实现。