一篇文章快速搞懂Java虚拟机的栈帧结构

简介: 栈帧(Stack Frame),就是Java虚拟机中的虚拟机栈(Virtual Machine Stack)的基本元素,它也是用于支持Java虚拟机进行方法调用和方法执行背后的数据结构,了解了它就可以更好地理解Java虚拟机执行引擎是如何运行的。

什么是栈帧?

正如大家所了解的,Java虚拟机的内存区域被划分为程序计数器、虚拟机栈、本地方法栈、堆和方法区。(什么?你还不知道,赶紧去看看《Java虚拟机内存结构及编码实战》)这次要介绍的栈帧(Stack Frame),就是Java虚拟机中的虚拟机栈(Virtual Machine Stack)的基本元素,它也是用于支持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虚拟机就会在类加载的校验阶段中抛出异常。

当一个方法被调用时,会使用局部变量表来完成参数值到参数变量列表的传递过程。如果执行的是对象实例的成员方法(没有被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

操作数栈(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虚拟机的实现是,都会进行一些优化:两个不同方法的栈帧出现一部分重叠。让下面栈帧的部分操作数栈与上面栈帧的部分局部变量表重叠在一起,这样做不仅节约了一些内存空间,更重要的是在进行方法调用时就可以直接共用一部分数据,不需要进行额外的参数复制和传递,如下图:

动态连接(Dynamic Linking)

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

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

方法返回地址

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

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

附加信息

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

总结

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

相关文章
|
6月前
|
存储 算法 Java
惊!Java程序员必看:JVM调优揭秘,堆溢出、栈溢出如何巧妙化解?
【8月更文挑战第29天】在Java领域,JVM是代码运行的基础,但需适当调优以发挥最佳性能。本文探讨了JVM中常见的堆溢出和栈溢出问题及其解决方法。堆溢出发生在堆空间不足时,可通过增加堆空间、优化代码及释放对象解决;栈溢出则因递归调用过深或线程过多引起,调整栈大小、优化算法和使用线程池可有效应对。通过合理配置和调优JVM,可确保Java应用稳定高效运行。
171 4
|
2月前
|
存储 监控 算法
深入探索Java虚拟机(JVM)的内存管理机制
本文旨在为读者提供对Java虚拟机(JVM)内存管理机制的深入理解。通过详细解析JVM的内存结构、垃圾回收算法以及性能优化策略,本文不仅揭示了Java程序高效运行背后的原理,还为开发者提供了优化应用程序性能的实用技巧。不同于常规摘要仅概述文章大意,本文摘要将简要介绍JVM内存管理的关键点,为读者提供一个清晰的学习路线图。
|
2月前
|
存储 算法 Java
Java 内存管理与优化:掌控堆与栈,雕琢高效代码
Java内存管理与优化是提升程序性能的关键。掌握堆与栈的运作机制,学习如何有效管理内存资源,雕琢出更加高效的代码,是每个Java开发者必备的技能。
84 5
|
3月前
|
JSON Java 程序员
Java|如何用一个统一结构接收成员名称不固定的数据
本文介绍了一种 Java 中如何用一个统一结构接收成员名称不固定的数据的方法。
43 3
|
3月前
|
Java
JVM运行时数据区(内存结构)
1)虚拟机栈:每次调用方法都会在虚拟机栈中产生一个栈帧,每个栈帧中都有方法的参数、局部变量、方法出口等信息,方法执行完毕后释放栈帧 (2)本地方法栈:为native修饰的本地方法提供的空间,在HotSpot中与虚拟机合二为一 (3)程序计数器:保存指令执行的地址,方便线程切回后能继续执行代码
34 3
|
4月前
|
缓存 算法 Java
JVM知识体系学习六:JVM垃圾是什么、GC常用垃圾清除算法、堆内存逻辑分区、栈上分配、对象何时进入老年代、有关老年代新生代的两个问题、常见的垃圾回收器、CMS
这篇文章详细介绍了Java虚拟机(JVM)中的垃圾回收机制,包括垃圾的定义、垃圾回收算法、堆内存的逻辑分区、对象的内存分配和回收过程,以及不同垃圾回收器的工作原理和参数设置。
155 4
JVM知识体系学习六:JVM垃圾是什么、GC常用垃圾清除算法、堆内存逻辑分区、栈上分配、对象何时进入老年代、有关老年代新生代的两个问题、常见的垃圾回收器、CMS
|
4月前
|
存储 算法 Java
聊聊jvm的内存结构, 以及各种结构的作用
【10月更文挑战第27天】JVM(Java虚拟机)的内存结构主要包括程序计数器、Java虚拟机栈、本地方法栈、Java堆、方法区和运行时常量池。各部分协同工作,为Java程序提供高效稳定的内存管理和运行环境,确保程序的正常执行、数据存储和资源利用。
85 10
|
3月前
|
存储 算法 Java
🧠Java零基础 - Java栈(Stack)详解
【10月更文挑战第17天】本文收录于「滚雪球学Java」专栏,专业攻坚指数级提升,希望能够助你一臂之力,帮你早日登顶实现财富自由🚀;同时,欢迎大家关注&&收藏&&订阅!持续更新中,up!up!up!!
80 2
|
4月前
|
存储 算法 Java
Java虚拟机(JVM)的内存管理与性能优化
本文深入探讨了Java虚拟机(JVM)的内存管理机制,包括堆、栈、方法区等关键区域的功能与作用。通过分析垃圾回收算法和调优策略,旨在帮助开发者理解如何有效提升Java应用的性能。文章采用通俗易懂的语言,结合具体实例,使读者能够轻松掌握复杂的内存管理概念,并应用于实际开发中。
|
4月前
|
存储 算法 Java
🚀Java零基础-顺序结构详解 🚀
【10月更文挑战第11天】本文收录于「滚雪球学Java」专栏,专业攻坚指数级提升,希望能够助你一臂之力,帮你早日登顶实现财富自由🚀;同时,欢迎大家关注&&收藏&&订阅!持续更新中,up!up!up!!
47 6