JVM学习-虚拟机栈内部结构

发布于:2024-05-16 ⋅ 阅读:(57) ⋅ 点赞:(0)

局部变量表

局部变量表也称为局部变量数组和本地变量表

  • 定义为一个数字数组,主要用于存储方法参数和定义在方法体内的局部变量,这些数据包括各类基本数据类型、对象引用,以及returnAddress类型。
  • 由于局部变量表是建立在线程的栈上,是线程私有数据,因此不存在数据安全问题
  • 局部变量表所需的容量大小是在编译期确定下来的,并保存在方法的Code属性的maxinum local variables数据项中,在方法运行期间是不会改变局部变量表的大小的。
  • 方法嵌套调用的次数由栈的大小决定,一般来说,栈越大,方法嵌套调用次数越多,对于一个函数而言,它的参数和局部变量越多,使得局部变量表膨胀,它的栈帧就越大,以满足方法调用所需传递的信息增大的需求,进而函数调用就会占用更多的栈空间,导致其嵌套调用次数就会减少。
  • 局部变量表中的变量只在当前方法调用中有效,在方法执行时,虚拟机通过使用局部变量表完成参数值到参数变量列表的传递过程,当方法调用结束后,随意方法栈帧的销毁,局部变量表也会随之销毁。
import java.util.Date;

/**
 * Administrator
 * 2024/5/16
 */
public class LocalVariablesTest {
    private int count = 0;

    public static void main(String[] args) {
        LocalVariablesTest test = new LocalVariablesTest();
        int num = 10;
        test.test1();
    }

    public void test1() {
        Date date = new Date();
        String name1 = "lotus.com";
        String info = test2(date,name1);
        System.out.println(date+name1);
    }

    public String test2(Date dateP, String name2) {
        dateP = null;
        name2 = "xudiudiu";
        double weight = 140.5;
        char gender = '男';
        return dateP + name2;
    }
    public void test3() {
        count++;
    }
    public void test4(){}
}
代码局部变量表分析
//反编译获取字节码信息
C:\Users\Administrator\IdeaProjects\jvm\target\classes\com\chapter05>javap -v LocalVariablesTest.class
//main方法
 public static void main(java.lang.String[]);
 LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0      16     0  args   [Ljava/lang/String;
            8       8     1  test   Lcom/chapter05/LocalVariablesTest;
           11       5     2   num   I[integer]

  public void test1();
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0      43     0  this   Lcom/chapter05/LocalVariablesTest;
            8      35     1  date   Ljava/util/Date;
           11      32     2 name1   Ljava/lang/String;
           18      25     3  info   Ljava/lang/String;

  public java.lang.String test2(java.util.Date, java.lang.String);
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0      33     0  this   Lcom/chapter05/LocalVariablesTest;
            0      33     1 dateP   Ljava/util/Date;
            0      33     2 name2   Ljava/lang/String;
            9      24     3 weight   D(double)
           14      19     5 gender   C(char)

  public void test3();
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0      11     0  this   Lcom/chapter05/LocalVariablesTest;

  public void test4();
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0       1     0  this   Lcom/chapter05/LocalVariablesTest;
}
SourceFile: "LocalVariablesTest.java"
slot
  • 参数值的存放总是在局部变量数组的index0开始,到数组长度-1的索引结束。
  • 局部变量表,最基本的存储单元是Slot(变量槽)
  • 局部变量表中存放编译期可知的各种基本数据类型(8种),引用数据类型,returnAddress类型的变量。
  • 在局部变量表中,32位以内的类型只占用一个slot(包含returnAddress类型),64位的类型(long和double)占用两个slot
    • byte,short,char在存储前被转换为int,boolean也被转换为int,0表示false,非0表示true
    • long,double则占用两个slot.
  • JVM为局部变量表中的每一个Slot都分配一个访问索引,通过这个索引即可成功访问到局部变量表中指定的局部变量值
  • 当一个实例方法被调用时,它的方法参数和方法体内部定义的局部变量将会按照顺序被复制到局部变量表中的每一个slot上
  • 如果需要访问局部变量表中的一个64bit的局部变量值时,只需要使用前一个索引即可
  • 如果当前帧是由构造方法或实例方法创建的,那么该对象引用this将会存放在index为0的slot处,其余参数按照参数表顺序继续排列
    test1方法的局部变量表,会将this放到index0位置,上图通过IDEA的jclasslib插件可以获得
slot重复利用

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

public class SlotTest {
    public void localVar1() {
        int a = 0;
        System.out.println(a);
        int b = 0;
    }
    public void localVar2() {
        {
            int a = 0;
            System.out.println(a);
        }
        //此时b会复用a的槽位
        int b = 0;
    }
}

槽位复用

静态变量与局部变量对比
  • 参数表分配完毕后,再根据方法体内定义的变量的顺序和作用域分配
  • 变量表有两次初使化的机会,第一次是“准备阶段[prepare]”,执行系统初使化,对类变量设置零值,另一次则是在“初使化[initial]”阶段,赋予程序员在代码中定义的初使值。
  • 和类变量初使化不同的是,局部变量表不存在系统初使化的过程,这意味着一旦定义了局部变量则必须人为的初始化,否则无法使用。
变量
  • 按数据类型分类
    • 基本数据类型
    • 引用数据类型
  • 按声明的位置
    • 成员变量
      • 类变量:第一次是“准备阶段[prepare]”,执行系统初使化,对类变量设置零值,另一次则是在“初使化[initial]”阶段,赋予程序员在代码中定义的初使值。
      • 实例变量:随着对象的创建,会在堆空间中分配 实例变量空间,并进行默认赋值
    • 局部变量:使用之前必须显示赋值,否则编译不通过
//没有赋值不能使用,在编译器中就会报错----Variable 'i' might not have been initialized
public void test() {
        int i;
        System.out.println(i);
    }

在栈帧中,与性能调优关系密切的部分是局部变量表,在方法执行时,虚拟机使用局部变量表完成方法的传递
局部变量表中的变量也是重要的垃圾回收根节点,只要被局部变量表中直接或间接引用的对象都不会被回收。

操作数栈(Operand Stack)

  • 每一个独立的栈帧中除了包含局部变量表以外,还包含一个后进先出的操作数栈
  • 操作数栈,在方法执行过程中,根据字节码指令,往栈中写入数据或提取数据,即入栈和出栈
    • 某些字节码指令将值压入操作数栈,其余的字节码指令将操作数取出栈,使用后再把结果压入栈
    • 比如执行复制、交换、求和等操作
  • 操作数栈,主要用于保存计算过程的中间结果,同时作为计算过程中变量临时的存储空间。
  • 操作数栈是JVM执行引擎的一个工作区,当一个方法则开始执行的时候,一个新的栈帧也会随之被创建出来,这个方法的操作数栈是空的
  • 每一个操作数栈拥有一个明确的栈深度用于存储数值,其所需的最大深度在编译期就定义好了,保存在方法的Code属性中,为max_stack的值
  • 栈中的任何一个元素都是可以任意Java数据类型
    • 32bit的类型占用一个栈单位深度
    • 64bit的类型占用两个栈单位深度
  • 操作数栈并非采用访问索引的方式来进行数据访问,只能通过标准的入栈和出栈操作来完成一次数据访问
  • 如果被调用的方法带有返回值的话,其返回值将会被压入当前栈帧的操作数栈中,并更新PC寄存器中下一条需要执行的字节码指令
  • 操作数栈中元素的数据类型必须与字节码指令的序列严格匹配,由编译器期间进行验证,同时在类加载过程中的类检验阶段的数据流分析阶段要再次验证
  • Java虚拟机的解释引擎是基于栈的执行引擎,栈即操作数栈
代码追踪
public void testAddOperation() {
       byte i = 15;
       int j = 8;
       int k = i + j;
   }

字节码指令

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

public void add() {
       //第1类
       int i1 = 10;
       int i2 = 10;
       i1++;
       ++i2;
       //第2类
       int i3 = 10;
       int i4 = i3++;
       
       int i5 = 10;
       int i6 = ++i5;
       //第3类
       int i7 = 10;
       i7 = i7++;
       
       int i8 = 10;
       i8 = ++i8;
       //第4类
       int i9 = 10;
       int i10 = i9++ + ++i9; 
   }