JVM_16_运行时栈帧结构

来源:互联网 发布:成都电脑编程培训 编辑:程序博客网 时间:2024/05/18 11:47

运行时栈帧结构


参考资料:

《图解JVM字节码执行引擎之栈帧结构》

《Java Virtual Machine Specification Java SE 7 》



栈帧

在之前《JVM_1_运行时内存区域》之中,我们讲解过栈、栈帧,我们先来回顾一下:

栈 《Java虚拟机规范 Java SE 7》一书中的说明:每一条Java虚拟机线程都有自己私有的Java虚拟机栈,这个栈与线程同时创建,用于存储栈帧。Java虚拟机栈的作用与传统语言(C语言)中的栈非常类似,就是用于存储局部变量与一些过程结果的地方。另外,它在方法调用和返回中也扮演了很重要的角色。因为除了栈帧的出栈和入栈之外,Java虚拟机栈不会受其他因素的影响,所以栈帧可以在堆中分配,Java虚拟机栈所使用的内存不需要保证是连续的。

栈帧《Java虚拟机规范 Java SE 7》一书中的说明:栈帧是用来存储数据和部分过程结果的数据结构,同时也被用来处理动态链接、方法返回值和异常分派。栈帧随着方法调用而创建,随着方法结束而销毁 ——无论方法是正常完成还是异常完成都算作方法结束。栈帧的存储空间分配在Java虚拟机栈之中,每一个栈帧都有自己的局部变量表、操作数栈和指向当前方法所属的类运行时常量池的引用。在一条线程中,只有目前正在执行的那个方法的栈帧是活动的。这个栈帧就被称作是 当前栈帧,这个栈帧对应的方法就被称为是当前方法,定义这个方法的类被称作当前类。对局部变量表和操作数栈的各种操作,通常都指的是对当前栈帧的局部变量表和操作数栈进行的操作。如果当前方法调用了其他方法,或者当前方法执行结束,那这个方法的栈帧就不再是当前栈帧了。当一个新的方法被调用,一个新的栈帧也会随之而创建,并随着程序控制权移交到新的方法而成为新的当前栈帧中。当方法返回之际,当前栈帧会传回此方法的执行结果,给前一个栈帧,在方法返回之后,当前栈帧就随之被丢弃,前一个栈帧就重新成为了当前栈帧需要特别注意的是:栈帧是线程本地私有的数据,不可能在一个栈帧之中引用另一条线程的栈帧。

JVM栈/Java栈 内部的结构如下图这个样子..

栈中包含栈帧;

栈帧中包含:局部变量表、操作数栈、动态链接、返回地址



局部变量表

局部变量表是一组变量值存储空间,用于存放方法参数和方法内部定义的局部变量。

在前面讲解类加载的时候,我们了解到类变量有两次赋初始值的过程

一次在准备阶段,赋予系统初始值;

另一次是在初始化阶段,赋予程序员多定义的初始值。


因此,即使在初始化阶段程序员没有为类变量赋值也没有关系,类变量任然具有一个确定的初始值

但局部变量就不一样了,如果一个局部变量定义了,但没有赋初始值是不能使用的,不要认为Java中任何情况下都存在诸如整型变量默认为0,

布尔型变量默认为false这样的默认值。

我们来看一个Demo:

我们在方法中定义一个局部变量,在编译期就会报错,编辑器能帮我们检查到,根据错误提示,我们要为局部变量赋值。

下面我们定一个类变量:

运行之后,结果输出为零,验证了上面的描述。

原书中关于"局部变量表"描述的还有很多,只是看的非常吃力,这里挑出重要的内容,了解。


---------------------------------------------------------------------------------------------------------------------


根据《Java虚拟机规范 Java SE 7》中的描述:


每个栈帧内部都包含一组称为局部变量表的变量列表。栈帧中局部变量表的长度由编译期决定,并且存储于类和接口的二进制表示之中。


一个局部变量可以保存一个类型为boolean、byte、short、float、reference、returnAddress的数据,

两个局部变量可以保存一个类型为long和double的数据。


局部变量使用索引来进行定位访问,第一个局部变量的索引值为零,局部变量的索引值是从零 到 小于局部变量表最大容量的所有整数。


long和double类型的数据占用两个连续的局部变量,这两种类型的数据值采用两个局部变量之中较小的索引值来定位。

栗如:

         一个double类型的值在索引值为n的局部变量中,实际上意思是索引值为n和n+1的两个局部变量都用来存储这个值。

         索引值为n+1的局部变量是无法直接读取的,但可能会被写入,只是如果进行了这种操作,就将会导致局部变量n的内容消失掉。


Java虚拟机使用局部变量表来完成方法调用时的参数传递,当一个方法被调用的时候,它的参数将会传递至从0开始的连续的局部变量表位置上。

特别的,当一个实例方法(非Static方法)被调用的时候,第0个局部变量一定是用来存储被调用的实例方法所在的对象的引用(即Java语言中的this关键字)。

(上面这一小段没太看懂..)



操作数栈

操作数栈也常称为操作栈,它是一个后入先出栈。同局部变量表一样,操作数栈的最大深度也在编译的时候确定的。

操作数栈的每一个元素可以是任意得到Java数据类型,包括long和double。32位数据类型所占的栈容量为1,64位数据类型所占的栈容量为2。


当一个方法刚刚开始执行的时候,这个方法的操作数栈是空的,在方法的执行过程中,会有各种字节码指令往操作数栈中写入和提取内容,也就是出栈/入栈操作。


------------------------------------------------------------------------------


《Java虚拟机规范 Java SE 7》中的描述:

每一个栈帧内部都包含一个称为操作数栈的 后进先出栈。栈帧中操作数栈的长度由编译器决定,并且存储于类和接口的二进制之中。

操作数栈 所属的 栈帧在刚刚被创建时,操作数栈是空的。(略微有点拗口,看仔细了...)

Java虚拟机提供了一些指令来从局部变量表或者对象实例的字段中复制常量或变量值到操作数栈中,也提供了一些指令用于从操作数栈取走数据、操作数据和把操作结果重新入栈。

在方法调用的时候,操作数栈也用来准备调用方法的参数以及接收方法返回的结果。

在操作数栈中的数据必须被正确的操作,这里正确操作是指对操作数栈必须与操作数栈顶的数据类型相匹配。

栗如:

不可以入栈两个int类型的数据,然后当做long类型去操作他们,或者入栈两个float类型的数据,然后使用iadd指令去对它们进行求和。

在任意时刻,操作数栈都会有一个确定的栈深度,一个long或者double会占用两个单位的栈深度,其他的数据类型会占用一个单位深度。



方法返回地址


当一个方法开始执行后,只有两种方式可以退出这个方法。

第一种是执行引擎遇到任意一个方法返回的字节码指令,这时候可能 会有返回值传递给上层的方法调用者。这种退出方法的方式成为正常完成出口。


另一种退出方式是,在方法执行过程中遇到了异常,并且这个异常没有在方法体内得到处理,无论是Java虚拟机内部产生的异常还是代码中athrow字节码指令产生的异常,只要在本方法的异常表中没有搜索到匹配的异常处理器,就会导致方法退出。这种退出方法的方式称为异常完成出口

一个方法使用异常完成出口的方式退出是不会给它的上层调用这产生任何返回值的


无论采用何种退出方式,在方法退出之后,都需要返回方法被调用的位置,程序才能继续执行,方法返回时可能需要在栈帧中保存一些信息,用来帮助恢复它的上层方法的执行状态。


一般来说,方法正常退出,调用者的PC计数器的值可以作为返回地址,栈帧中很可能会保存这个计数器值。

而方法异常退出时,返回地址是要通过异常处理器表来确定的,栈帧中一般不会保存这部分信息。


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


------------------------------------------------------------------------


《Java虚拟机规范 Java SE 7》中的描述:

方法正常调用完成:

方法正常调用完成是指在方法的执行过程中,没有任何异常抛出。(包括直接从Java虚拟机之中抛出的异常以及在执行时通过throw语句显示抛出的异常)

如果当前方法正常完成的话,他很可能会返回一个值给调用它的方法。


在这种场景下,当前栈帧承担着回复调用者状态的职责,其状态包括调用者的局部变量表、操作数栈和被正确增加过来表示执行了该方法调用指令的程序计数器等。


方法异常调用完成:

方法异常调用完成是指 在方法的执行过程中,某些指令导致了Java虚拟机抛出异常,并且虚拟机抛出的异常在该方法中没有办法处理,或者执行过程中遇到了athrow字节码指令显式的抛出异常,并且在该方法内部没有把异常捕获住。

如果方法异常调用完成,那一定不会有方法返回值返回给它的调用者。



在网上找到了这个图,感觉不错,比较详细,可以对照着看看...


原创粉丝点击