JVM 栈帧结构详解

在 Java 虚拟机(JVM)中,栈帧(Stack Frame)是用于支持方法调用和方法执行的关键数据结构。每个方法从调用开始到执行完成,都对应着一个栈帧在虚拟机栈中从入栈到出栈的过程。本文将详细介绍 JVM 栈帧的结构及其组成部分。

1 栈帧的组成

每个栈帧包含以下几个主要部分:

  • 局部变量表(Local Variables Table)
  • 操作数栈(Operand Stack)
  • 动态链接(Dynamic Linking)
  • 方法返回地址(Return Address)
  • 附加信息(Additional Information)

2 局部变量表(Local Variables Table)

局部变量表用于存储方法中的局部变量和方法参数。在 Java 源代码被编译成字节码文件时,局部变量表的最大容量就已经确定。

2.1 示例代码

考虑以下代码片段:

public class LocalVariablesTable {private void write(int age) {String name = "沉默王二";}
}

write() 方法有一个参数 age 和一个局部变量 name

2.2 使用 jclasslib 查看字节码

使用 jclasslib 工具查看编译后的字节码文件 LocalVariablesTable.class,可以看到 write() 方法的 Code 属性中,Maximum local variables 的值为 3。
在这里插入图片描述

理论上,局部变量表的最大容量应该是 2(一个 age,一个 name),但实际值为 3。这是因为非静态方法的局部变量表中,第 0 个位置存储的是调用该方法的对象引用 this。因此,调用 write(18) 实际上是调用 write(this, 18)

查看 Code 属性中的 LocalVariableTable,可以看到详细信息:
在这里插入图片描述

  • 第 0 个是 this,类型为 LocalVariablesTable 对象。
  • 第 1 个是方法参数 age,类型为整型 int
  • 第 2 个是方法内部的局部变量 name,类型为字符串 String

2.3 局部变量表的大小与作用域

局部变量表的大小不仅取决于局部变量的数量,还与变量的类型和作用域有关。当一个局部变量的作用域结束时,它占用的局部变量表位置会被后续的局部变量取代。

考虑以下代码:

public static void method() {// ①if (true) {// ②String name = "沉默王二";}// ③if (true) {// ④int age = 18;}// ⑤
}

在这里插入图片描述

  • method() 方法的局部变量表大小为 1,因为它是静态方法,不需要添加 this 作为局部变量表的第一个元素。
  • 在 ② 处,局部变量 name 的作用域开始,局部变量表的大小为 1。
  • 在 ③ 处,局部变量 name 的作用域结束。
  • 在 ④ 处,局部变量 age 的作用域开始,局部变量表的大小为 1。
  • 在 ⑤ 处,局部变量 age 的作用域结束。

《Effective Java》中的第 57 条建议:

将局部变量的作用域最小化,可以增强代码的可读性和可维护性,并降低出错的可能性。

为了节省栈帧的内存空间,局部变量表中的槽是可以重用的。合理的作用域设计有助于提高程序性能。

2.4 局部变量表的槽(Slot)

局部变量表的容量以槽(slot)为最小单位,一个槽可以容纳一个 32 位的数据类型(如 int)。对于占用 64 位的数据类型(如 doublelong),会占用两个紧挨着的槽。

考虑以下代码:

public void slot() {double d = 1.0;int i = 1;
}

使用 jclasslib 查看 slot() 方法的字节码,可以看到 Maximum local variables 的值为 4。
在这里插入图片描述

为什么是 4 呢?加上 this 也只有 3 个变量。查看 LocalVariableTable 可以看到,变量 i 的下标为 3,这意味着变量 d 占了两个槽。
在这里插入图片描述

3 操作数栈(Operand Stack)

操作数栈是 JVM 栈帧中的一个重要组成部分,用于在方法执行过程中存储操作数和中间计算结果。与局部变量表类似,操作数栈的最大深度在编译时就已经确定,并写入到方法表的 Code 属性的 maximum stack size 中。

3.1 示例代码

考虑以下代码片段:

public class OperandStack {public void test() {add(1, 2);}private int add(int a, int b) {return a + b;}
}

OperandStack 类包含两个方法:test()add()test() 方法调用了 add() 方法,并传递了两个参数。

3.2 使用 jclasslib 查看字节码

使用 jclasslib 工具查看 test() 方法的字节码,可以看到 maximum stack size 的值为 3。
在这里插入图片描述

这是因为调用成员方法时,会将 this 和所有参数压入操作数栈中。调用完毕后,this 和参数会依次出栈。通过 Bytecode 面板可以查看对应的字节码指令:
在这里插入图片描述

  • aload_0:将局部变量表中下标为 0 的引用类型的变量(即 this)加载到操作数栈中。
  • iconst_1:将整数 1 加载到操作数栈中。
  • iconst_2:将整数 2 加载到操作数栈中。
  • invokevirtual:调用对象的成员方法。
  • pop:将栈顶的值出栈。
  • return:为 void 方法的返回指令。

3.3 add() 方法的字节码指令

再来看一下 add() 方法的字节码指令:
在这里插入图片描述

  • iload_1:将局部变量表中下标为 1 的 int 类型变量(即参数 a)加载到操作数栈上(下标为 0 的是 this)。
  • iload_2:将局部变量表中下标为 2 的 int 类型变量(即参数 b)加载到操作数栈上。
  • iadd:用于 int 类型的加法运算。
  • ireturn:为返回值为 int 的方法返回指令。

3.4 数据类型匹配

操作数栈中的数据类型必须与字节码指令匹配。例如,iadd 指令只能用于整型数据的加法运算。在执行 iadd 指令时,栈顶的两个数据必须是 int 类型,不能出现一个 long 型和一个 double 型的数据进行 iadd 命令相加的情况。

4 动态链接(Dynamic Linking)

动态链接是 JVM 栈帧中的一个重要概念,它允许在运行时根据对象的实际类型来解析方法调用。每个栈帧都包含一个指向运行时常量池中该栈帧所属方法的引用,这个引用用于支持方法调用过程中的动态链接。

4.1 运行时常量池与方法区

在深入理解动态链接之前,我们需要了解两个关键概念:

  1. 方法区(Method Area)

    • 方法区是 JVM 的一个运行时内存区域,用于存储已被虚拟机加载的类信息、常量、静态变量以及即时编译器编译后的代码等。
    • 不同版本的 JDK 对方法区的实现可能有所不同,但其主要功能是相同的。
  2. 运行时常量池(Runtime Constant Pool)

    • 运行时常量池是方法区的一部分,用于存放编译期生成的各种字面量和符号引用。
    • 在类加载后,这些字面量和符号引用会被加载到运行时常量池中。

4.2 示例代码

考虑以下代码片段:

public class DynamicLinking {static abstract class Human {protected abstract void sayHello();}static class Man extends Human {@Overrideprotected void sayHello() {System.out.println("男人哭吧哭吧不是罪");}}static class Woman extends Human {@Overrideprotected void sayHello() {System.out.println("山下的女人是老虎");}}public static void main(String[] args) {Human man = new Man();Human woman = new Woman();man.sayHello();woman.sayHello();man = new Woman();man.sayHello();}
}

在这段代码中,ManWoman 类继承了 Human 类,并重写了 sayHello() 方法。运行结果如下:

男人哭吧哭吧不是罪
山下的女人是老虎
山下的女人是老虎

从面向对象编程的角度来看,这个结果是符合预期的。manwoman 的引用类型都是 Human,但它们分别指向 ManWoman 对象。之后,man 被重新指向 Woman 对象。

4.3 字节码分析

使用 jclasslib 工具查看 main 方法的字节码指令:
在这里插入图片描述

  1. 第 1 行new 指令创建了一个 Man 对象,并将对象的内存地址压入栈中。
  2. 第 2 行dup 指令将栈顶的值复制一份并压入栈顶。因为接下来的 invokespecial 指令会消耗掉一个当前类的引用,所以需要复制一份。
  3. 第 3 行invokespecial 指令用于调用构造方法进行初始化。
  4. 第 4 行astore_1 指令将栈顶的 Man 对象引用弹出,并存入下标为 1 的局部变量 man 中。
  5. 第 5-8 行:与第 1-4 行类似,不同的是创建了 Woman 对象。
  6. 第 9 行aload_1 指令将局部变量 man 压入操作数栈中。
  7. 第 10 行invokevirtual 指令调用对象的成员方法 sayHello(),注意此时的对象类型为 com/itwanger/jvm/DynamicLinking$Human
  8. 第 11 行aload_2 指令将局部变量 woman 压入操作数栈中。
  9. 第 12 行:与第 10 行相同,调用 sayHello() 方法。

4.4 动态链接的解析过程

从字节码的角度来看,man.sayHello()woman.sayHello() 的字节码指令是完全相同的,但它们最终执行的目标方法却不同。这是因为 invokevirtual 指令在运行时的解析过程如下:

  1. 找到操作数栈顶的元素所指向的对象的实际类型,记作 C
  2. 在类型 C 中查找与常量池中的描述符匹配的方法
    • 如果找到匹配的方法,则进行访问权限校验。如果通过,则返回该方法的直接引用,查找结束。
    • 如果未通过校验,则抛出 java.lang.IllegalAccessError 异常。
  3. 如果未找到匹配的方法,则按照继承关系从下往上依次对 C 的各个父类进行第二步的搜索和验证
  4. 如果始终没有找到合适的方法,则抛出 java.lang.AbstractMethodError 异常

4.5 动态链接的本质

invokevirtual 指令在第一步就确定了运行时的实际类型,因此它并不是简单地将常量池中的符号引用解析为直接引用就结束了。它会根据方法接收者的实际类型来选择方法版本,这个过程就是 Java 重写的本质。我们把这种在运行期根据实际类型确定方法执行版本的过程称为动态链接。

5 方法返回地址

在 JVM 中,方法的退出有两种方式:正常退出异常退出。无论哪种方式,方法退出后都需要返回到方法最初被调用时的位置,以便程序能够继续执行。方法返回地址是栈帧中的一个重要组成部分,用于记录方法退出后控制流应返回的位置。

5.1 正常退出

当方法正常退出时,可能会有返回值传递给上层的方法调用者。方法是否有返回值以及返回值的类型由方法返回的指令决定。常见的返回指令包括:

  • ireturn:用于返回 int 类型。
  • lreturn:用于返回 long 类型。
  • freturn:用于返回 float 类型。
  • dreturn:用于返回 double 类型。
  • areturn:用于返回引用类型。
  • return:用于 void 方法的返回。

在方法正常退出时,PC(Program Counter)计数器的值会作为返回地址,栈帧中通常会保存这个计数器的值。返回地址记录了方法调用后下一条指令的位置,以便程序能够继续执行。

5.2 异常退出

当方法在执行过程中遇到异常且未被妥善处理时,方法会异常退出。在这种情况下,方法不会给上层调用者返回任何值。异常退出时,PC 计数器的值不会作为返回地址保存,因为异常处理机制会接管控制流,跳转到异常处理代码。

5.3 方法退出的过程

方法退出的过程实际上等同于将当前栈帧从栈中弹出(出栈)。出栈后,JVM 会执行以下操作:

  • 恢复上层方法的局部变量表和操作数栈:当前方法的栈帧出栈后,上层方法的栈帧成为当前栈帧,JVM 会恢复上层方法的局部变量表和操作数栈。
  • 将返回值压入调用者栈帧的操作数栈中:如果当前方法有返回值,JVM 会将返回值压入调用者栈帧的操作数栈中。
  • 调整 PC 计数器的值:PC 计数器的值会被更新为方法调用后的下一条指令的地址,以便程序能够继续执行。

5.4 PC 计数器

PC 计数器是 JVM 运行时数据区的一部分,用于跟踪当前线程执行字节码的位置。每个线程都有自己的 PC 计数器,记录当前执行的字节码指令的地址。

5.5 方法退出的示例

考虑以下代码片段:

public class MethodReturnExample {public static void main(String[] args) {int result = add(1, 2);System.out.println(result);}public static int add(int a, int b) {return a + b;}
}

main 方法中,调用了 add(1, 2) 方法。add 方法的返回值为 3,并通过 ireturn 指令返回给 main 方法。main 方法接收到返回值后,将其存储在局部变量 result 中,并打印输出。

5.6 异常退出的示例

考虑以下代码片段:

public class ExceptionExample {public static void main(String[] args) {try {throwException();} catch (Exception e) {System.out.println("Exception caught: " + e.getMessage());}}public static void throwException() throws Exception {throw new Exception("Test exception");}
}

throwException 方法中,抛出了一个异常。由于 main 方法中捕获了该异常,throwException 方法会异常退出。异常退出时,throwException 方法不会返回任何值给 main 方法,而是由异常处理机制接管控制流,跳转到 catch 块中执行。

6 附加信息

虚拟机规范允许具体的虚拟机实现增加一些额外的信息到栈帧中,例如与调试相关的信息。这些信息完全取决于具体的虚拟机实现。

7 StackOverflowError

StackOverflowError 是 Java 中常见的运行时异常,通常发生在递归调用过深或方法调用链过长,导致栈内存溢出时。下面通过两个示例代码来分析 StackOverflowError 的产生原因及堆栈信息。

7.1 示例代码 1:无限制递归调用

public class StackOverflowErrorTest {public static void main(String[] args) {StackOverflowErrorTest test = new StackOverflowErrorTest();test.testStackOverflowError();}public void testStackOverflowError() {testStackOverflowError();}
}

运行结果

运行上述代码时,会抛出 StackOverflowError 异常。

异常原因

testStackOverflowError() 方法是一个递归方法,它在方法体内直接调用了自身,且没有终止条件。每次调用 testStackOverflowError() 方法时,JVM 都会为该方法创建一个新的栈帧,并将其压入当前线程的栈中。由于递归调用没有终止条件,栈帧会不断累积,最终导致栈内存溢出,抛出 StackOverflowError 异常。

堆栈信息

异常堆栈信息如下:

Exception in thread "main" java.lang.StackOverflowErrorat com.yunyang.javabetter.jvm.stackframe.StackOverflowErrorTest.testStackOverflowError(StackOverflowErrorTest.java:17)at com.yunyang.javabetter.jvm.stackframe.StackOverflowErrorTest.testStackOverflowError(StackOverflowErrorTest.java:17)at com.yunyang.javabetter.jvm.stackframe.StackOverflowErrorTest.testStackOverflowError(StackOverflowErrorTest.java:17)at com.yunyang.javabetter.jvm.stackframe.StackOverflowErrorTest.testStackOverflowError(StackOverflowErrorTest.java:17)at com.yunyang.javabetter.jvm.stackframe.StackOverflowErrorTest.testStackOverflowError(StackOverflowErrorTest.java:17)...

从堆栈信息中可以看到,testStackOverflowError() 方法被重复调用,且每次调用的位置都在第 17 行(即方法体内的递归调用)。

7.2 示例代码 2:带计数的递归调用

为了更好地观察递归调用的次数,我们对代码进行了简单改造:

public class StackOverflowErrorTest1 {private static AtomicInteger count = new AtomicInteger(0);public static void main(String[] args) {while (true) {testStackOverflowError();}}public static void testStackOverflowError() {System.out.println(count.incrementAndGet());testStackOverflowError();}
}

运行结果

在运行上述代码时,程序会不断递归调用 testStackOverflowError() 方法,并打印递归调用的次数。当递归调用达到一定次数时,栈内存溢出,抛出 StackOverflowError 异常。

在我的本地环境中,递归调用次数达到 11457 次时,抛出了 StackOverflowError 异常。

异常原因

与示例代码 1 类似,testStackOverflowError() 方法是一个递归方法,它在方法体内直接调用了自身,且没有终止条件。每次调用时,JVM 都会为该方法创建一个新的栈帧,并将其压入当前线程的栈中。由于递归调用没有终止条件,栈帧会不断累积,最终导致栈内存溢出,抛出 StackOverflowError 异常。

堆栈信息

异常堆栈信息如下:

11456
11457
Exception in thread "main" java.lang.StackOverflowErrorat sun.nio.cs.UTF_8$Encoder.encodeLoop(UTF_8.java:691)at java.nio.charset.CharsetEncoder.encode(CharsetEncoder.java:579)at sun.nio.cs.StreamEncoder.implWrite(StreamEncoder.java:271)...

从堆栈信息中可以看到,递归调用次数达到 11457 次时,栈内存溢出,抛出 StackOverflowError 异常。

8 总结

栈帧是 JVM 中用于方法执行的核心数据结构,每个方法调用都会创建一个栈帧,并在方法执行完毕后销毁。栈帧的局部变量表和操作数栈的大小在编译时确定,动态链接支持方法调用中的多态性,方法返回地址记录了方法结束后的控制流位置。理解栈帧对于深入理解 Java 程序的运行机制至关重要。

9 思维导图

在这里插入图片描述

10 参考链接

深入理解 JVM 的栈帧结构

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.rhkb.cn/news/490104.html

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

python学opencv|读取图像(七)抓取像素数据顺利修改图像大小

【1】引言 前序我们已经学习图像的基本读取操作,文章链接为: python学opencv|读取图像-CSDN博客 也掌握了彩色图像的保存: python学opencv|读取图像(二)保存彩色图像_python opencv 读取图像转为彩色-CSDN博客 以…

CSS|07 标准文档流

标准文档流 一、什么是标准文档流 在制作的 HTML 网页和 PS 画图软件画图时有本质上面的区别: HTML 网页在制作的时候都得遵循一个“流的规则:从左至右、从上至下。 使用 Ps 软件画图时可以在任意地方画图。 <!DOCTYPE html> <html lang"en"> <hea…

git的卸载与安装

目录 一、Git的卸载 二、Git的安装 2.1.1 官网下载 2.1.2 镜像下载 ​编辑 2.2 安装 2.3 检验否安装成功 三、Git使用配置 一、Git的卸载 1.找到程序&#xff0c;卸载程序 2.找到Git&#xff0c;右键卸载 卸载完成&#xff01; 二、Git的安装 2.1.1 官网下载 网址&…

探索 Echarts 绘图:数据可视化的奇妙之旅

目录 一、Echarts 初印象 二、搭建 Echarts 绘图环境 三、绘制第一个图表&#xff1a;柱状图的诞生 四、图表的美化与定制&#xff1a;让数据更具吸引力 1. 主题切换&#xff1a;一键变换风格 2. 颜色调整&#xff1a;色彩搭配的艺术 3. 标签与提示框&#xff1a;丰富信…

location重定向和nginx代理

文章目录 1 location重定向1.1 概述1.2 rewrite跳转1.3 用例1.4 实验1.4.1 基于域名的跳转1.4.2 基于ip的跳转1.4.3 基于后缀名的跳转 2 nginx的代理2.1 nginx内置变量2.2 正向代理2.2.1 固定正向代理2.2.2 自动代理 2.3 反向代理2.3.1 负载均衡的算法2.3.2 负载均衡的特点2.3.…

前端-自定义Ant Design 表格(可编辑表格)

选取的的是&#xff1a;表格 Table - Ant Design 其实ant design本身就有增加和删除单列数据的封装好的表格&#xff0c;但是个人觉得那个功能繁多&#xff0c;自己实现封装也便于之后理解和二次使用。 初步效果&#xff08;舍去切换样式的功能&#xff09;&#xff1a; 突破的…

通过ajax的jsonp方式实现跨域访问,并处理响应

一、场景描述 现有一个项目A&#xff0c;需要请求项目B的某个接口&#xff0c;并根据B接口响应结果A处理后续逻辑。 二、具体实现 1、前端 前端项目A发送请求&#xff0c;这里通过jsonp的方式实现跨域访问。 $.ajax({ url:http://10.10.2.256:8280/ssoCheck, //请求的u…

AI监控赋能健身馆与游泳馆全方位守护,提升安全效率

一、AI视频监控技术的崛起 随着人工智能技术的不断发展&#xff0c;AI视频监控正成为各行业保障安全、提升效率的关键工具。相比传统监控系统&#xff0c;AI技术赋予监控系统实时分析、智能识别和精准预警的能力&#xff0c;让“被动监视”转变为“主动防控”。 二、AI监控应用…

Maven完整技术汇总

额外知识点 IDE IDE是集成开发环境的缩写&#xff0c;它是一种软件应用程序&#xff0c;提供了编码、调试和部署软件的一站式解决方案。这些功能集成在一起&#xff0c;使开发人员能够在一个环境中完成整个软件开发过程&#xff0c;从编写代码到调试和测试&#xff0c;直到最终…

细说STM32F407单片机SPI基础知识

目录 一、 SPI接口和通信协议 1、 SPI硬件接口 &#xff08;1&#xff09;MOSI(Master Output Slave Input) &#xff08;2&#xff09;MISO(Master Input Slave Output) &#xff08;3&#xff09;SCK 2、SPI传输协议 &#xff08;1&#xff09;CPHA0时的数据传输时序 …

华为自反ACL实验

一、实验背景 做这个实验的原因是最近公司里上了三台小程序服务器&#xff0c;由于三台服务器的端口都映射出去了&#xff0c;领导要求A网段的三台服务器不能访问内网B&#xff0c;C网段&#xff0c;同时B、C网段内网用户可以访问A段的94、95、96服务器&#xff1b; 也就是PC4\…

geeCache 一致性hash

目标&#xff1a;解决当自身结点没有改缓存时&#xff0c;从哪个结点获得这个缓存的问题 一、普通的hash算法 hash(Tom)%结点数量 缺点&#xff1a;缓存雪崩 缓存雪崩是指在某一时刻&#xff0c;大量缓存同时失效或宕机&#xff0c;导致大量请求直接访问数据库&#xff0c;从…

rabbitMq举例

新来个技术总监&#xff0c;把 RabbitMQ 讲的那叫一个透彻&#xff0c;佩服&#xff01; 生产者 代码举例 public String sendMsg(final String exchangeName,final String routingKey,final String msg) {} /*** 发送消息* param exchangeName exchangeName* param routin…

HNSW 分布式构建实践

作者&#xff1a;魏子敬 一、背景 随着大模型时代的到来&#xff0c;向量检索领域面临着前所未有的挑战。embedding 的维度和数量空前增长&#xff0c;这在工程上带来了极大的挑战。智能引擎事业部负责阿里巴巴搜推广及 AI 相关工程系统的设计和建设&#xff0c;我们在实际业务…

Windows安装elasticsearch、Kibana以及IK分词器

一、下载 1.下载elasticsearch 访问官网Download Elasticsearch | Elastic&#xff0c;下载elasticsearch 2.下载 Kibana 访问Download Kibana Free | Get Started Now | Elastic &#xff0c;下载 Kibana 3. IK分词器下载 访问Gitee 极速下载/elasticsearch-analysis-ik选…

1125 子串与子列 (暴力搜索,PAT甲级中文版,C++实现)

子串是一个字符串中连续的一部分&#xff0c;而子列是字符串中保持字符顺序的一个子集&#xff0c;可以连续也可以不连续。例如给定字符串 atpaaabpabtt&#xff0c;pabt是一个子串&#xff0c;而 pat 就是一个子列。 现给定一个字符串 S 和一个子列 P&#xff0c;本题就请你找…

低通滤波器,高通滤波器,公式

1 低通滤波器 &#xff1a;输出的是电容的电压 1 低通滤波器可以把低频信号上面的高频信号给滤掉 2 100hz正常通过 3 经过低通滤波器后&#xff0c;波形光滑&#xff0c;绿色波形。一致 4 电容充电速度跟不上输入信号的速度&#xff08;因为加了电阻&#xff0c;限制了电流&…

前端样式练手:阴阳图+时钟的组合

开篇 今天的小作品是突然脑子灵光一闪写出来的&#xff0c;代码不多&#xff0c;就不过多赘述了。 代码实现 <template><div class"clock-container"><!-- 八卦图 --><!-- <div class"bagua"><divv-for"(trigram, ind…

拟合与滤波算法:(四)中值滤波

中值滤波 1&#xff09;算法说明 把测量值放在一个数组里&#xff0c;设置一个长度为 m m m 的小窗&#xff0c;令它在数组上滑动&#xff0c;对窗口内的值进行排序&#xff0c;用中间值替换原来的数据&#xff08;窗口中间的原数据&#xff09;&#xff1b;每次滑动一格&am…

Deepmotion技术浅析(四):人体姿态估计

人体姿态估计是 DeepMotion 动作捕捉和 3D 重建流程中的核心模块之一。该模块的主要任务是从输入的视频帧中检测并定位人体关键点&#xff08;如关节、头部、手脚等&#xff09;的位置。DeepMotion 的人体姿态估计模块不仅支持 2D 关键点检测&#xff0c;还能够进行 3D 关键点估…