java虚拟机详解篇十一(java虚拟机栈)

java虚拟机栈

虚拟机栈出现的背景:java语言是跨平台设计的,java的指令是根据栈设计的,不同的平台不同的CPU架构不同,所以java不能基于寄存器设计。优点是跨平台,指令集小,编译器容易实现,缺点是性能下降,实现同样的功能需要更多的指令。

虚拟机栈:它与程序计数器一样,Java虚拟机栈(Java Virtual Machine Stack)也是线程私有的,它的生命周期与线程相同。虚拟机栈描述的是Java方法执行的线程内存模型:每个方法被执行的时候,Java虚拟机都会同步创建一个栈帧l (Stack Frame)用于存储局部变量表、操作数栈、动态连接、方法出口等信息。每一个方法被调用直至执行完毕的过程,就对应着一个栈帧(栈帧是方法运行期很重要的基础数据结构)在虚拟机栈中从入栈到出栈的过程。(java虚拟机栈的访问速度仅次与程序计数器,而且对栈来说不存在垃圾回收问题)
在这里插入图片描述
模拟栈异常情况:

/**
 *  栈异常情况
 *  默认情况下:count : 11420
 *  设置栈的大小: -Xss256k : count : 2465
 */
public class StackErrorTest {
    private static int count = 1;
    public static void main(String[] args) {
        System.out.println(count);
        count++;
        main(args);// 本机最大到11420附近
    }
}

在这里插入图片描述

虚拟机栈帧的内存分配:有些人认为Java内存区域只有堆内存((Heap〉和栈内存(Stack),这种划分方式直接继承自传统的C、C++程序的内存布局结构(因为当java面世时,C/C++正处于顶峰时期),在Java语言里就显得有些粗糙了,实际的内存区域划分要比这更复杂。“栈”通常指虚拟机栈,或者更多的情况下只是指虚拟机栈中局部变量表部分。

public class StackError {
    private static int a = 0;
    public void stackOutError(){
        a++;
        stackOutError();
    }

    public static void main(String[] args) {
    	// 函数调用的最大深度,一般都是在这附近了。
        StackError error = new StackError();
        try {
            error.stackOutError();
        }catch (Throwable e){
            System.out.println("The last a count is = "+a);
            e.printStackTrace();
        }
    }
}

在这里插入图片描述
局部变量槽:这些数据类型在局部变量表中的存储空间以局部变量槽(Slot)来表示,其中64位长度的long和double类型的数据会占用两个变量槽,其余的数据类型只占用一个。局部变量表所需的内存空间在编译期间完成分配,当进入一个方法时,这个方法需要在栈帧中分配多大的局部变量空间是完全确定的,在方法运行期间不会改变局部变量表的大小。“大小”是指变量槽的数量,虚拟机真正使用多大的内存空间(譬如按照1个变量槽占用32个比特、64个比特,或者更多)来实现一个变量槽,这是完全由具体的虚拟机实现自行决定的事情。

局部变量槽的复用对垃圾回收机制的影响之一

为了尽可能节省栈帧耗用的内存空间,局部变量表中的变量槽是可以重用的,方法体中定义的变量,其作用域并不一定会覆盖整个方法体,如果当前字节码PC计数器的值已经超出了某个变量的作用域,那这个变量对应的变量槽就可以交给其他变量来重用。不过,这样的设计除了节省栈帧空间以外,还会伴随有少量额外的副作用,而且在某些情况下变量槽的复用会直接影响到系统的垃圾收集行为。

虚拟机栈扩展小结:在《Java虚拟机规范》中,对这个内存区域规定了两类异常状况:如果线程请求的栈深度大于虚拟机所允许的深度,将抛出StackOverflowError异常;如果Java虚拟机栈容量可以动态扩展,当栈扩展时无法申请到足够的内存会抛出OutOfMemoryError异常。

模拟StackOverFlowError

public class StackOverFlowTest {
    private static int a = 0;
    public static void main(String[] args) {
        try{
            new StackOverFlowTest().test1();
        }catch (Exception e){
            e.printStackTrace();
            System.out.println("a最后的值是:"+a);
        }
    }
    public void test1(){
        a++;
        test2();
    }
    public void test2(){
        a++;
        test1();
    }
}

在这里插入图片描述
模拟OutOfMemoryError异常

import java.util.ArrayList;
import java.util.List;

public class OutOfMemory {
    public static void main(String[] args) {
        List<String> list = new ArrayList<>();
        for (int i = 0; ; i++){
            list.add(new String("OutOfMemory异常"));
        }
    }
}

在配置虚拟机内存的区域限制大小,-Xmx1m:(设置大小为1mb)
在这里插入图片描述
在这里插入图片描述
HotSpot虚拟机的栈容量是不可以动态扩展的,以前的Classic虚拟机倒是可以。所以在HotSpot虚拟机上是不会由于虚拟机栈无法扩展而导致OutOfM emoryError异常,只要线程申请栈空间成功了就不会有OOM,但是如果申请时就失败,仍然是会出现OOM异常的。

public class VirtualStack01 {
    public void methodA(){
        int a = 1;
        int b = 10;
        methodB();
    }

    public void methodB(){
        int k = 2;
        int m = 20;
    }

    public static void main(String[] args) {
        VirtualStack01 stack01 = new VirtualStack01();
        stack01.methodA();
    }
    /**
     * {
     *   public CrazyTest.VirtualStackTest.VirtualStack01();
     *     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   LCrazyTest/VirtualStackTest/VirtualStack01;
     *
     *   public void methodA();
     *     descriptor: ()V
     *     flags: ACC_PUBLIC
     *     Code:
     *       stack=1, locals=3, args_size=1
     *          0: iconst_1
     *          1: istore_1
     *          2: bipush        10
     *          4: istore_2
     *          5: aload_0
     *          6: invokevirtual #2                  // Method methodB:()V 调用方法B
     *          9: return
     *       LineNumberTable:
     *         line 5: 0
     *         line 6: 2
     *         line 7: 5
     *         line 8: 9
     *       LocalVariableTable:
     *         Start  Length  Slot  Name   Signature
     *             0      10     0  this   LCrazyTest/VirtualStackTest/VirtualStack01;
     *             2       8     1     a   I
     *             5       5     2     b   I
     *
     *   public void methodB();
     *     descriptor: ()V
     *     flags: ACC_PUBLIC
     *     Code:
     *       stack=1, locals=3, args_size=1
     *          0: iconst_2
     *          1: istore_1
     *          2: bipush        20
     *          4: istore_2
     *          5: return
     *       LineNumberTable:
     *         line 11: 0
     *         line 12: 2
     *         line 13: 5
     *       LocalVariableTable:
     *         Start  Length  Slot  Name   Signature
     *             0       6     0  this   LCrazyTest/VirtualStackTest/VirtualStack01;
     *             2       4     1     k   I
     *             5       1     2     m   I
     *
     *   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           #3    创建对象              // class CrazyTest/VirtualStackTest/VirtualStack01
     *          3: dup
     *          4: invokespecial #4                  // Method "<init>":()V
     *          7: astore_1
     *          8: aload_1
     *          9: invokevirtual #5                  // Method methodA:()V
     *         12: return   // 设定返回值,默认添加的。
     *       LineNumberTable:
     *         line 16: 0
     *         line 17: 8
     *         line 18: 12
     *       LocalVariableTable:
     *         Start  Length  Slot  Name   Signature
     *             0      13     0  args   [Ljava/lang/String;
     *             8       5     1 stack01   LCrazyTest/VirtualStackTest/VirtualStack01;
     * }
     */

}

运行时栈帧的内存结构

java程序的每一个线程都有自己的栈,栈中的数据都是以栈帧的格式存在,在这个线程上执行的每一个方法都各自对应一个栈帧,栈帧是一个内存块,也可看作一个数据集,存储着方法执行的过程中的各种数据信息。(方法调用对应于栈帧在虚拟机栈的入栈和出栈的过程)

虚拟机栈帧的内存结构

1、局部变量表
2、操作数栈
3、动态链接
4、方法返回地址
5、附加信息

在这里插入图片描述
每一个方法从调用开始至执行结束,都对应着一个栈帧在虚拟机栈中的从入栈到出栈的过程。

编译java程序的时候,栈帧中需要多大的局部变量表,需要多深的操作数栈就已经被分析计算出来,并且写入到方法表的Code属性之中。换言之,一个栈帧需要分配多少内存,并不会受到程序运行期变量数据的影响,而仅仅取决于程序源码以及具体的虚拟机实现的栈内存布局形式。

以Java程序的角度来看,同一时刻、同一条线程里面,在调用堆栈的所有方法都同时处于执行状态。而对于执行引擎来讲,在活动线程中,只有位于栈顶的方法才是在运行的,只有位于栈顶的栈帧才是生效的,其被称为“当前栈帧”(Current Stack Frame),与这个栈帧所关联的方法被称为“当前方法”(Current Method)。执行引擎所运行的所有字节码指令都只针对当前栈帧进行操作,如果在该方法中调用了其他的方法,对应的新的栈帧就会被创建出来,放在栈的顶部,成为新的当前帧。

public class JavaTest {
    public static void main(String[] args) {
        try {
       // 方法的结束方式分为两种:
       // ① 正常结束,以return为代表 
       // ② 方法执行中出现未捕获处理的异常,以抛出异常的方式结束
            JavaTest test  =new JavaTest();
            test.methodA();
            /**
             * method1()开始执行...
             * method2()开始执行...
             * method3()开始执行...
             * method3()即将结束...
             * method2()即将结束...
             * method1()执行结束...
             * main方法结束。。。。。。
             */
        }catch (Exception e){
            e.printStackTrace();
        }

        System.out.println("main方法结束。。。。。。");
    }

    public void methodA(){
        System.out.println("method1()开始执行...");
        methodB();
        System.out.println("method1()执行结束...");
    }

    public int methodB(){
        System.out.println("method2()开始执行...");
        int i = 10;
        int m = (int) methodC();
        System.out.println("method2()即将结束...");
        return i + m;

    }

    public double methodC(){
        System.out.println("method3()开始执行...");
        double j = 20.0;
        System.out.println("method3()即将结束...");
        return j;
    }
}

在这里插入图片描述
不同的线程中所包含的栈帧是不允许存在相互引用的,即不可能在一个栈帧中引用另外一个线程的栈帧。

如果当前方法调用了其他的方法,方法返回之际,当前帧会传回此方法的执行结果给前一个栈帧,然后虚拟机会丢弃当前帧,使得前一个栈帧重新成为当前栈帧。

java方法有两种函数返回的方式:一个是正常的是函数返回,return指令,另外一种是抛出异常,这两种方式都会导致虚拟机的栈帧弹出

局部变量表(Local Variables Table)

局部变量表被称为局部变量数组和本地变量表,局部变量表被定义为一个数字数组,主要存储方法参数和定义在方法体内的局部变量,这些数据类似是包含了各种的基本数据类型,对象引用(reference)和returnAddress类型

由于局部变量是建立在线程的栈之上,是线程的私有数据,因此不存在数据安全问题。

方法嵌套调用的次数由栈大小决定,一般来说,栈越大,方法嵌套调用的次数越多对于一个函数而言,它的参数和局部变量越多,使得局部变量表膨胀,它的栈帧就越大,以满足方法调用所需传递的信息增大的需求,进而函数调用就会占用更多的栈空间,导致其嵌套调用的次数就会减少。

局部变量表的变量只是在当前方法调用中有效,在方法执行时,虚拟机通过使用局部变量表来完成参数值到参数变量表的传递过程,当方法调用结束后,随着方法栈帧的销毁,局部变量表也会随之销毁。

参数值总会存放在局部变量表数组的index0的开始,到数组的长度减一的索引结束。

局部变量表,最基本的存储单元是Slot(变量槽)

局部变量表中存放编译期可知的8种局基本的数据类型,引用类型(reference)和returnAddress类型的变量。
在局部变量表中,32位以内的类型只是占一个slot(包括returnAdres类型),64位类型的(long和double)占用两个slot。

注意:byte,short,char,在存储前被转换为intboolean也被转换为int类型,0代表false,1代表truelong和double则占用两个slot

JVM会为局部变量表中的每一个slot分配一个访问索引,通过这个索引可以访问到局部变量表中指定的局部变量值。

当一个实例方法被调用时,它的方法参数和方法体内定义的局部变量将会按照顺序被复制到局部变量表中的每一个slot上。

如果需要访问局部变量表中一个64bit的局部变量值时,只需要使用前一个索引。

局部变量表(Local Variables Table)是一组变量值的存储空间,用于存放方法参数和方法内部定义的局部变量。在Java程序被编译为Class文件时,就在方法的Code属性的max_locals数据项中确定了该方法所需分配的局部变量表的最大容量。

当一个方法被调用时,Java虚拟机会使用局部变量表来完成参数值到参数变量列表的传递过程,即实参到形参的传递。如果执行的是实例方法(没有被static修饰的方法),那局部变量表中第0位索引的变量槽默认是用于传递方法所属对象实例的引用,在方法中可以通过关键字“this”来访问到这个隐含的参数。其余参数则按照参数表顺序排列,占用从1开始的局部变量槽,参数表分配完毕后,再根据方法体内部定义的变量顺序和作用域分配其余的变量槽。
如果当前帧是由构造方法或者实例方法创建的,那么该对象的引用this将会存放在index0的slot处。其余的将会按照参数表顺序排列。

在栈帧中,与性能调优关系最为密切的部分就是局部变量表。在方法执行时,(虚拟机使用局部变量表完成方法的传递。)

局部变量表中的变量也是重要的垃圾回收根节点,只要被局部变量表中直接或间接引用的对象都不会被回收。
在这里插入图片描述
参数表分配完毕之后,再根据方法体内定义的变量的顺序和作用域分配。类变量表有两次初始化的机会,第一次是在“准备阶段”,执行系统初始化,对类变量设置零值,另一次则是在“初始化”阶段,赋予程序员在代码中定义的初始值。和类变量初始化不同的是,局部变量表不存在系统初始化的过程,这意味着一旦定义了局部变量则必须人为的初始化,否则无法使用。
在这里插入图片描述
上面的输出会报错,变量m没有初始化。

public class LocalVariablesTest {
    private int count = 0;

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

    //练习:
    public static void testStatic(){
        LocalVariablesTest test = new LocalVariablesTest();
        Date date = new Date();
        int count = 10;
        System.out.println(count);
        //因为this变量不存在于当前方法的局部变量表中!!
//        System.out.println(this.count);
    }

    //关于Slot的使用的理解
    public LocalVariablesTest(){
        this.count = 1;
    }

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

    public String test2(Date dateP, String name2) {
        dateP = null;
        name2 = "woshinibaba";
        double weight = 130.5;//占据两个slot
        char gender = '男';
        return dateP + name2;
    }

    public void test3() {
        this.count++;
    }

    public void test4() {
        int a = 0;
        {
            int b = 0;
            b = a + 1;
        }
        //变量c使用之前已经销毁的变量b占据的slot的位置
        int c = a + 1;
    }

    /*
    变量的分类:按照数据类型分:① 基本数据类型  ② 引用数据类型
                按照在类中声明的位置分:① 成员变量:在使用前,都经历过默认初始化赋值
                                                类变量: linking的prepare阶段:给类变量默认赋值  ---> initial阶段:给类变量显式赋值即静态代码块赋值
                                                实例变量:随着对象的创建,会在堆空间中分配实例变量空间,并进行默认赋值
                                       ② 局部变量:在使用前,必须要进行显式赋值的!否则,编译不通过
     */
    public void test5Temp(){
        int num;
        //System.out.println(num);//错误信息:变量num未进行初始化
    }

}

操作数栈(Operand Stack)(或表达式栈)

操作数栈(Operand Stack)也常被称为操作栈,它是一个后入先出(Last In First Out,LIFO)栈。同局部变量表一样,操作数栈的最大深度也在编译的时候被写入到Code属性的max_stacks数据项之中。操作数栈的每一个元素都可以是包括long和double在内的任意Java数据类型。

操作数栈,主要用于保存计算过程的中间结果,同时作为计算过程中变量临时的存储空间。操作数栈就是JVM执行引擎的一个工作区,当一个方法刚开始执行的时候,一个新的栈帧也会随之被创建出来,这个方法的操作数栈是空的。

32bit的类型占用一个栈单位深度
64bit的类型占用两个栈单位深度

操作数栈并非采用访问索引的方式来进行数据访问的,而是只能通过标准
入栈(push)出栈(pop)操作来完成一次数据访问。如果被调用的方法带有返回值的话,其返回值将会被压入当前栈帧的操作数栈中,并更新PC寄存器中下一条需要执行的字节码指令。

操作数栈中元素的数据类型必须与字节码指令的序列严格匹配,这由编译器在编译器期间进行验证,同时在类加载过程中的类检验阶段的数据流分析阶段要再次验证。另外,我们说Java虚拟机的解释引擎是基于栈的执行引擎,其中的栈指的就是操作数栈。

当一个方法刚刚开始执行的时候,这个方法的操作数栈是空的,在方法的执行过程中,会有各种字节码指令往操作数栈中写入和提取内容,也就是出栈和入栈操作。譬如在做算术运算的时候是通过将运算涉及的操作数栈压入栈顶后调用运算指令来进行的,又譬如在调用其他方法的时候是通过操作数栈来进行方法参数的传递。举个例子,例如整数加法的字节码指令iadd,这条指令在运行的时候要求操作数栈中最接近栈顶的两个元素已经存入了两个int型的数值,当执行这个指令时,会把这两个int值出栈并相加,然后将相加的结果重新入栈。iadd这个指令只能用于整型数的加法,它在执行时,最接近栈顶的两个元素的数据类型必须为int型,不能出现一个long和一个float使用iadd命令相加的情况。

另外在概念模型中,两个不同栈帧作为不同方法的虚拟机栈的元素,是完全相互独立的。但是在大多虚拟机的实现里都会进行一些优化处理,令两个栈帧出现一部分重叠。让下面栈帧的部分操作数栈与上面栈帧的部分局部变量表重叠在一起,这样做不仅节约了一些空间,更重要的是在进行方法调用时就可以直接共用一部分数据,无须进行额外的参数复制传递了。

在这里插入图片描述

动态链接(Dynamic Linking)(指向运行时常量池的引用)

每个栈帧都包含一个指向运行时常量池 中该栈帧所属方法的引用,持有这个引用是为了支持方法调用过程中的动态连接(Dynamic Linking)。Class文件的常量池中存有大量的符号引用,字节码中的方法调用指令就以常量池里指向方法的符号引用作为参数。这些符号引用一部分会在类加载阶段或者第一次使用的时候就被转化为直接引用,这种转化被称为静态解析。另外一部分将在每一次运行期间都转化为直接引用,这部分就称为动态连接。

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

方法返回地址(Return Address)(方法正常退出或者异常退出的定义)

当一个方法开始执行后,只有两种方式退出这个方法。第一种方式是执行引擎遇到任意一个方法返回的字节码指令(return指令),这时候可能会有返回值传递给上层的方法调用者(调用当前方法的方法称为调用者或者主调方法),方法是否有返回值以及返回值的类型将根据遇到何种方法返回指令来决定,这种退出方法的方式称为“正常调用完成”(Normal Method Invocation Completion)。

另外一种退出方式是在方法执行的过程中遇到了异常,并且这个异常没有在方法体内得到妥善处理。无论是Java虚拟机内部产生的异常,还是代码中使用athrow字节码指令产生的异常,只要在本方法的异常表中没有搜索到匹配的异常处理器,就会导致方法退出,这种退出方法的方式称为“异常调用完成(Abrupt Method Invocation Completion)”。一个方法使用异常完成出口的方式退出,是不会给它的上层调用者提供任何返回值的。

无论采用何种退出方式,在方法退出之后,都必须返回到最初方法被调用时的位置,程序才能继续执行,方法返回时可能需要在栈帧中保存一些信息,用来帮助恢复它的上层主调方法的执行状态。一般来说,方法正常退出时,主调方法的PC计数器的值就可以作为返回地址,栈帧中很可能会保存这个计数器值。而方法异常退出时,返回地址是要通过异常处理器表来确定的,栈帧中就一般不会保存这部分信息。

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

附加信息

《Java虚拟机规范》允许虚拟机实现增加一些规范里没有描述的信息到栈帧之中,例如与调试、性能收集相关的信息,这部分信息完全取决于具体的虚拟机实现,一般会把动态连接、方法返回地址与其他附加信息全部归为一类,称为栈帧信息。

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
Java虚拟可以读取字节码文件并将其转换成可执行的代码。字节码文件是Java源代码编译后生成的二进制文件,它包含了一系列指令,这些指令被Java虚拟解释和执行。通过这种方式,Java程序可以在不同的硬件平台和操作系统上运行,实现了"Write Once, Run Anywhere"的目标。 Java虚拟读取字节码文件的过程可以简单概括为以下几个步骤: 1. 加载:Java虚拟通过类加载器加载字节码文件,将其转换为运行时的类对象。类加载器负责查找并加载类文件,并将其转换为内存中的类对象。 2. 验证:在加载字节码文件后,Java虚拟会对字节码文件进行验证,确保其符合Java语言规范和虚拟规范。验证过程包括对字节码文件的结构、语义和安全性进行检查。 3. 准备:在验证通过后,Java虚拟会为类变量(静态变量)分配内存,并设置默认初始值。此时,还没有执行任何Java代码。 4. 解析:在准备阶段之后,Java虚拟会对字节码文件中的符号引用进行解析,将其转换为直接引用。这个过程将类或接口的符号引用解析为实际的内存地址。 5. 初始化:在准备阶段之后,Java虚拟会执行类的初始化操作,包括执行静态初始化块和静态变量的赋值操作。在这个阶段,Java程序的主方法会被调用,程序开始执行。 通过以上步骤,Java虚拟可以读取字节码文件并执行其中的指令,实现Java程序的运行。<span class="em">1</span><span class="em">2</span><span class="em">3</span> #### 引用[.reference_title] - *1* *2* *3* [Java 进阶:实例详解 Java 虚拟字节码指令](https://blog.csdn.net/m0_54853420/article/details/126104672)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v93^chatsearchT3_2"}}] [.reference_item style="max-width: 100%"] [ .reference_list ]

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值