憨人笔记之JVM-运行时数据区(虚拟机栈)

247 阅读9分钟

话不多说,干就完了。

虚拟机栈

虚拟机栈它描述的是Java方法执行的内存模型。它的基本组成是栈帧。一个栈帧就对应的一个Java方法,任何一个Java方法在执行的时候都一定会有对应的栈帧。栈帧同程序计数器一样,也是属于线程私有的,每个线程在创建的时候都会创建一个栈帧。

栈帧的作用在于保存程序运行时候方法的局部变量、部分结果并参与方法的调用与返回,刚刚说到,在虚拟机栈中,一个方法会对应着一个栈帧,方法的执行与方法的返回就对应着栈帧在虚拟机栈中的出栈与入栈。

特点

  • 虚拟机栈的访问速度仅次于程序计数器,是一种快速高效的存储方式
  • 虚拟机栈是基于栈的数据结构,所以它只会有两种操作入栈(方法执行)、出栈(方法返回)
  • 虚拟机栈不属于GC的范围

栈实际上是属于一种数据结构,那么它也会有大小的限制。在虚拟机栈中,它的大小模式有两种,一种是动态模式,另一种就是固定大小模式。正对不同的大小设置模式,会存在不同的错误。

  • 动态模式

    当虚拟机栈采用动态模式进行栈深度的设置时,如果方法在执行时无法申请到足够的内存大小,或者说在创建新的线程时没有足够的大小去创建新的栈,则会抛出OutOfMemoryError异常。

  • 固定大小

    当虚拟机菜哦那个固定大小的模式设置栈的深度,如果线程请求分配的内存空间超过了栈设定的最大容量时候,就会抛出StackOverflowError异常。栈的固定大小可以通过-Xss参数来设置。

上面说到,栈的操作只有入栈和出栈,而所操作的元素就是栈帧。

栈帧-虚拟机栈的基本存储单位

栈帧是虚拟机栈的最基本存储单位,每个栈帧都对应了这个线程上正在执行的方法。栈帧它是一个内存区域,一个存储的数据集。保存的方法执行过程中的各种数据信息。

虚拟机对栈的操作只有两个,就是对栈帧的出栈和入栈,遵循先进后出/后进先出的这么一个原则。而对于一个线程来说,在一个时间点只会有一个活动的栈帧,也就是说只有当前正在执行的方法的栈帧(栈顶栈帧)是有效的,也叫做当前栈帧。与当前栈帧相对应的就叫做当前方法,而定义该方法的类就叫做当前类

在执行引擎中运行的所有字节码指令都是只正对当前栈帧的操作。如果存在方法嵌套(方法中调用另外一个方法),那么被调用的方法就会被作为一个新的栈帧,在被调用执行的时候放在栈顶。

入栈对应着方法的执行,而出栈存在两种情况,一种是方法的正常返回(return)另一种就是方法中抛出了未被捕获处理的异常,这两种情况下栈帧都会被从栈中弹出(出栈)。

刚刚说到,栈帧实际上是一个数据集,下面详细讲讲栈帧中的数据集包括了哪些数据:

  • 局部变量表(本地变量表)

    局部变量表也称为局部变量数组或本地变量表。它定义为一个数字数组,主要存储的是方法的入参数以及定义在方法内部的局部变量。数据类型包括**基本数据类型、对象的引用以及返回地址类型。**一个局部变量可以保存boolean、byte、float、short、char、int和reference、returnAddress数据,对于long、double类型的数据会占用两个局部变量。

    局部变量表是建立在线程的栈的栈帧之上的,所以也就不存在数据安全的问题。局部变量表在编译期间就确定了大小并完成成了内存分配,所以在程序运行期间局部变量表的大小是不会再被改变的。

    局部变量表中的基本存储单元为Slot,可以理解为上面所说的局部变量,对于一个32位的数据类型,占用一个Slot,而对于64位的数据类型,占用两个Slot(long、double)。

    在局部变量表中,虚拟机是通过索引定位的方式来访问局部变量表中的变量,也就是Slot。其索引由0开始到局部变量表最大Slot数量。上面说到32位和64位数据所占据的大小不同,那么在索引访问的时候,如果是32位的数据,其索引就是n,对于64位的数据,其索引位置为n,n+1两个Slot。需要注意的是,如果当前栈帧是通过构造方法或者实例方法创建的,那么在局部变量表index位0的位置,会保存改对象的引用this。

    栈帧中的Slot是可以重复利用的,局部变量表中的变量也是重要的垃圾回收跟节点(GC Roots),只要是局部变量表中直接或者间接引用的对象都不会被回收。

  • 操作数栈

    操作数栈主要用于保存计算过程中的中间结果,同时作为计算过程中临时变量的存储空间,可以将操作数栈看作成一个方法执行的真正的工作区。它也是一个先进先出的栈结构。在执行方法的过程中,根据字节码指令往操作数栈中写入或提取数据(分别对应着入栈和出栈)。在新的栈帧被创建的时候,其操作数栈是空的。任何一个操作数栈都会有一个明确的栈深度用于存储方法执行过程中的数据。与局部变量表一样,操作数栈的大小在编译的时候就确定了大小

    操作数栈中的元素可以是任意的Java数据类型,同局部变量表一样,64位的数据类型占用两个栈单位。但是其访问方式并不是像局部变量表一样通过所以访问的。操作数栈的访问是通过标准的入栈和出栈来进行数据访问的。

  • 动态链接

    动态链接也可以称作为指向运行时常量池的方法引用。在虚拟机栈的栈帧中,每一个栈帧都包含了指向运行时常量池中该栈帧所属方法的引用。对于字面意思,可能不好理解,下面通过一段代码及反编译后的字节码文件来进行解释:

    /**
     * @description: 操作数栈测试类
     * @author: 请叫我憨人先森
     * @create: 2020-03-22 14:32
     **/
    public class OperandStackTest {
    
        public void method() {
            int i = 18;
            int j = 22;
            int k = i + j;
        }
    
        public void method2() {
            method();
        }
    
    }
    

    反编译后的结果解读:

在上述代码中,我们在method2方法中调用了method方法,将该类文件通过javap反编译得到下面的字节码文件,我们可以看到。method2的字节码区域,其调用了method方法,而它所存储的就是指向运行时常量池中方法的引用,也就是#2,通过#2找到该常量池中对应的符号引用#3、#21,也就对应步骤2,按照此规律,最终定位到了#6(方法返回值"()V"),V表示返回值为void,而#12则表示方法名。

  • 方法调用

    在虚拟机中,将符号引用转变成调用方法的直接引用与方法的绑定机制是密切相关的。需要注意,方法的调用并不等于方法的执行,方法的调用阶段唯一的任务就是确定调用哪个方法。任何方法在Class文件里面存储的都是符号引用。而不是方法实际运行时内存的入口地址(即直接引用)。

    回顾之前的类加载过程,在解析阶段,会将常量池中的符号引用转换成直接引用。而这通常是针对于在编译阶段就已经确定下来的方法(主要包括静态方法和私有方法)。

    静态方法与类型有关,而私有方法在外部不能访问,这两者的特性决定了它们不能通过继承或者别的方式重写方法的版本。所以他们都适合在类加载阶段进行解析。

  • 方法返回地址

    方法返回地址也就是调用该方法的方法在程序计数器中的值,一个方法的结束有两种形式,一种是正常执行完成返回,另一种就是在执行过程中发生了未被捕获的异常,导致方法异常退出。而对于正常执行完成的方法,返回地址记录的就是调用者在程序计数器中的值,也就是调用该方法的执行的下一条指令的地址。而对于异常退出的方法,返回地址是通过一场表来确定的,在栈帧中一般不会保存这部分信息。

    通俗点讲,就是在方法A中调用方法B,方法B正常返回后,回到方法A的方法体内,而方法A下一步该怎么做,执行哪一行代码,方法返回地址就是记录的这个信息。

总结

通过以上知识,对最开始的图进行细化总结:


不怕路歹行不怕大雨淋,心上一字敢 面对我的梦,甘愿来作憨人。 --<憨人>