欢迎关注微信公众号:万猫学社,每周一分享Java技术干货。

什么是栈帧?

正如大家所了解的,Java虚拟机的内存区域被划分为程序计数器、虚拟机栈、本地方法栈、堆和方法区。(什么?你还不知道,赶紧去看看《Java虚拟机内存结构及编码实战》)这次要介绍的栈帧(Stack Frame),就是Java虚拟机中的虚拟机栈(Virtual Machine Stack)的基本元素,它也是用于支持Java虚拟机进行方法调用和方法执行背后的数据结构,了解了它就可以更好地理解Java虚拟机执行引擎是如何运行的。

每一个方法从调用开始至执行结束的整个过程,都对应着一个栈帧在虚拟机栈中从入栈到出栈的过程。栈帧存储了方法的局部变量表、操作数栈、动态连接和方法返回地址等信息,在同一时刻、同一条线程中,只有位于栈顶的方法才是在运行的,只有位于栈顶的栈帧才是生效的,执行引擎所运行的所有字节码指令都只针对当前栈帧进行操作。虚拟机栈和栈帧的总体结构如下图:

接下来,再分别介绍一下栈帧中的局部变量表、操作数栈、动态连接、方法返回地址等各个部分的作用和数据结构。

欢迎关注微信公众号:万猫学社,每周一分享Java技术干货。

局部变量表(Local Variables Table)

局部变量表是用来存储一组变量值的内存空间,用于存放方法参数和方法内部定义的局部变量。在已经编译好的Class文件中,方法的Code属性的max_locals数据项中,就确定了该方法所需分配的局部变量表的最大容量。

局部变量表的容量以变量槽(Variable Slot)为最小单位,每个变量槽存放一个32位数据类型,如boolean、byte、char、short、int、float和reference这几种类型。前6种类型同学们应该都了解,就不必多介绍了,reference类型表示对一个对象实例的引用,通过这个引用做到两件事情:根据引用直接或间接地查找到实例在Java堆中的数据存放的起始地或索引;根据引用直接或间接地查找到在方法区中的存储的类信息。对于64位数据类型,如long和double这两种类型,是以高位对齐的方式为其分配两个连续的变量槽空间。

使用局部变量表时,通过索引定位对应数据的位置,索引值的范围是从0开始至局部变量表最大的变量槽数量。如果访问的是32位数据类型的变量,索引N就代表了使用第N个变量槽,如果访问的是64位数据类型的变量,则说明会同时使用第N和N+1两个变量槽。对于两个相邻的共同存放一个64位数据的两个变量槽,虚拟机不允许采用任何方式单独访问其中的某一个,如果遇到进行这种操作的字节码,Java虚拟机就会在类加载的校验阶段中抛出异常。

欢迎关注微信公众号:万猫学社,每周一分享Java技术干货。

当一个方法被调用时,会使用局部变量表来完成参数值到参数变量列表的传递过程。如果执行的是对象实例的成员方法(没有被static修饰的方法),那么局部变量表中第0位索引的变量槽默认就是该对象实例的引用,在方法中可以通过关键字this来访问到这个隐含的参数。其余参数则按照参数表顺序排列,参数表分配完毕后,再根据方法体内部定义的局部变量顺序和作用域分配其余的变量槽。为了尽可能节省栈帧所耗的内存空间,局部变量表中的变量槽是可以重用的,当方法体中定义的局部变量超出其作用域时,该局部变量对应的变量槽就可以交给其他变量来重用。

之前的《JVM的类加载机制详解》中介绍过,在类加载过程中,类变量有两次赋初始值的过程,一次在准备阶段,赋予系统初始值;另外一次在初始化阶段,赋予代码中定义的初始值。因此即使没有为类变量赋值也没有关系,类变量仍然具有一个确定的初始值,不会产生歧义。但是局部变量不像类变量有那样的“准备阶段”,如果一个局部变量定义了但没有赋初始值,那它是完全不能使用的。所以不要认为Java中任何情况下都存在诸如整型变量默认为0、布尔型变量默认为false等这样的默认值规则。比如:

public class OneMoreStudy {
public static void main(String[] args) {
int i;
System.out.println(i);
}
}

因为局部变量i没有初始,在编译过程就会报错:

Error:(4, 28) java: 可能尚未初始化变量i

欢迎关注微信公众号:万猫学社,每周一分享Java技术干货。

操作数栈(Operand Stack)

操作数栈是一个后入先出(Last In First Out,LIFO)栈。和局部变量表一样,在已经编译好的Class文件中,方法的Code属性的max_stacks数据项中,就确定了该方法所需分配的操作数栈的最大深度。在方法执行的任何时候,操作数栈的深度都不会超过在max_stacks数据项中设定的最大值。操作数栈的每一个元素都可以是包括long和double在内的任意Java数据类型。32位数据类型所占的栈容量为1,64位数据类型所占的栈容量为2。

当一个方法刚刚开始执行的时候,该方法的操作数栈是空的,在该方法的执行过程中,会有各种字节码指令对操作数栈进行出栈和入栈的操作。比如,整数加法的字节码指令iadd,在该指令执行前必须保证操作数栈中最接近栈顶的两个元素已经存入了两个int型的数值,当该指令执行时,会把这两个int值出栈并相加,然后将相加的结果重新入栈。

操作数栈中元素的数据类型必须与字节码指令的序列严格匹配,在编译代码时,编译器会严格保证这一点,在类加载的校验阶段也会再次验证这一点。在上面的iadd指令中,只能用于整型数的加法,它在执行时,最接近栈顶的两个元素的数据类型必须为int型,不能出现其他数据类型使用iadd命令相加的情况。

一个方法调用另外一个方法时,可以通过操作数栈来进行方法参数的传递。虽然在Java虚拟机规范中,两个不同栈帧作为不同方法的虚拟机栈的元素,是完全相互独立的。但是在大多Java虚拟机的实现时,都会进行一些优化:两个不同方法的栈帧出现一部分重叠。让下面栈帧的部分操作数栈与上面栈帧的部分局部变量表重叠在一起,这样做不仅节约了一些内存空间,更重要的是在进行方法调用时就可以直接共用一部分数据,不需要进行额外的参数复制和传递,如下图:

欢迎关注微信公众号:万猫学社,每周一分享Java技术干货。

动态连接(Dynamic Linking)

每个栈帧都包含一个指向运行时常量池中该栈帧所属方法的引用,持有这个引用是为了支持方法调用过程中的动态连接。

之前的《Class文件结构全面解析》中介绍过,Class文件的常量池中存有大量的符号引用,这些符号引用一部分会在类加载阶段或者第一次使用的时候就被转化为直接引用(实际运行时内存布局中的入口地址),这种转化被称为静态解析。另外一部分将在每一次运行期间都转化为直接引用,这部分就称为动态连接。关于这两个转化过程的具体过程,这里先卖个关子,后续的文章会详细介绍。

欢迎关注微信公众号:万猫学社,每周一分享Java技术干货。

方法返回地址

方法返回时可能需要在栈帧中保存一些信息,用来于恢复调用者(调用当前方法的方法)的执行状态。一般来说,方法正常退出时,调用者的程序计数器的值就可以作为返回地址,栈帧中很可能会保存这个计数器值。而方法异常退出时,返回地址是要通过异常处理器表来确定的,栈帧中就一般不会保存这部分信息。

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

欢迎关注微信公众号:万猫学社,每周一分享Java技术干货。

附加信息

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

欢迎关注微信公众号:万猫学社,每周一分享Java技术干货。

总结

栈帧是Java虚拟机中的虚拟机栈的基本元素,每一个方法从调用开始至执行结束的整个过程,都对应着一个栈帧在虚拟机栈中从入栈到出栈的过程。栈帧存储了方法的局部变量表、操作数栈、动态连接和方法返回地址和其他附加信息。局部变量表用于存放方法参数和方法内部定义的局部变量;各种字节码指令执行时,会对操作数栈进行出栈和入栈的操作;动态连接是指向运行时常量池中该栈帧所属方法的引用;方法返回地址用于恢复调用当前方法的方法的执行状态。

欢迎关注微信公众号:万猫学社,每周一分享Java技术干货。

详细解析Java虚拟机的栈帧结构的更多相关文章

  1. Java虚拟机之栈帧

    写在前面的话:Java虚拟机是一门学问,是众多Java大神们的杰作,由于我个人水平有限,精力有限,不能保证所有的东西都是正确的,这里内容都是经过深思熟虑的,部分引用原著的内容,讲的已经很好了,不在累述 ...

  2. Java虚拟机运行时栈帧结构--《深入理解Java虚拟机》学习笔记及个人理解(二)

    Java虚拟机运行时栈帧结构(周志明书上P237页) 栈帧是什么? 栈帧是一种数据结构,用于虚拟机进行方法的调用和执行. 栈帧是虚拟机栈的栈元素,也就是入栈和出栈的一个单元. 2018.1.2更新(在 ...

  3. 深入理解java虚拟机(十) Java 虚拟机运行时栈帧结构

    运行时栈帧结构 栈帧(Stack Frame) 是用于虚拟机执行时方法调用和方法执行时的数据结构,它是虚拟栈数据区的组成元素.每一个方法从调用到方法返回都对应着一个栈帧入栈出栈的过程. 每一个栈帧在编 ...

  4. 第7篇-为Java方法创建栈帧

    在 第6篇-Java方法新栈帧的创建 介绍过局部变量表的创建,创建完成后的栈帧状态如下图所示. 各个寄存器的状态如下所示. // %rax寄存器中存储的是返回地址 rax: return addres ...

  5. 转:二十一、详细解析Java中抽象类和接口的区别

    转:二十一.详细解析Java中抽象类和接口的区别 http://blog.csdn.net/liujun13579/article/details/7737670 在Java语言中, abstract ...

  6. C语言函数调用及栈帧结构

    source:http://blog.csdn.net/qq_29403077/article/details/53205010 一.地址空间与物理内存 (1)地址空间与物理内存是两个完全不同的概念, ...

  7. 【转载】深入理解Java虚拟机笔记---运行时栈帧结构

    栈帧(Stack Frame)是用于支持虚拟机进行方法调用和方法执行的数据结构,它是虚拟机运行时数据区的虚拟机栈(Virtual Machine Stack)的栈元素.栈帧存储了方法的局部变量表,操作 ...

  8. 图解JVM字节码执行引擎之栈帧结构

    一.执行引擎      “虚拟机”的概念是相对于“物理机”而言的,这两种“机器”都有执行代码的能力.物理机的执行引擎是直接建立在硬件处理器.物理寄存器.指令集和操作系统层面的:而“虚拟机”的执行引擎是 ...

  9. 深入解析java虚拟机-jvm运行机制

    转自oschina 一:JVM基础概念 JVM(Java虚拟机)一种用于计算设备的规范,可用不同的方式(软件或硬件)加以实现.编译虚拟机的指令集与编译微处理器的指令集非常类似.Java虚拟机包括一套字 ...

随机推荐

  1. js中的数据类型及常用属性和方法

    JavaScript 字符串 字符串(或文本字符串)是一串字符(比如 "Bill Gates").字符串被引号包围.您可使用单引号或双引号您可以在字符串内使用引号,只要这些引号与包 ...

  2. tp5 thinkphp5 多表关联查询 join查询

    model下: $res = \think\Db::name('article') ->alias("a") //取一个别名 ->join('admin ad','a. ...

  3. codeforces 609C

    #include<bits/stdc++.h> using namespace std; ],c[]; int main() { int n,i; while(cin >> n ...

  4. HDU 1051

    题意:给你n个木块的长和宽,现在要把它送去加工,这里怎么说呢,就是放一个木块花费一分钟,如果后面木块的长和宽大于等于前面木块的长和宽就不需要花费时间,否则时间+1,问把这个木块送去加工的最短时间. 思 ...

  5. H3C 高级ACL部署位置示例

  6. SQL2008 R2安装完成后开启services服务指引和 sa账号启用、数据类型

  7. 怎样在RxJS Observable中使用Async-Await

    怎样在RxJS Observable中使用Async-Await 一般情况下 async-await 和 Observables 并不能“在一起使用”.但RxJS 从一开始就具备与 Promises ...

  8. document.getElementById()

    使用两个for循环取json数据的时候出错: 代码简化如下: for(var a=0;a<3;a++){ for(var b=0;b<3;b++){ document.getElement ...

  9. jquery核心基础

    jquery对对象的操作:   检查对象类型: 老式的javascript使用typeOf()操作符,但他是不符合逻辑的,在某些情况下,typeOf()返回的不是一个正确的值,或者返回一个出乎意料的值 ...

  10. C# 如何解析XML