【JVM】从i++到JVM栈帧
本篇博客将用两个代码例子,简单认识一下JVM与栈帧结构以及其作用
从i++与++i说起
先不急着看i++和++i,我们来看看JVM虚拟机(请看VCR.JPG)
我们初学JAVA的时候一定都听到过JAVA“跨平台”的特性,也就是说,我们的代码可以直接在windows上运行,也可以直接在Linux上运行,而这种特性与虚拟机(也就是JVM)息息相关。
那么JVM这么强大,其内部内存结构是怎样的?笔者将在后面向大家简单介绍。
光知道跨平台与JVM息息相关,便足够了吗?
——鲁迅
正如鲁迅所说,写程序不能只知其然而不知其所以然,我们来看看JVM的内存模型(运行时数据区)。
由于篇幅限制(其实是笔者没学过),本篇博客将会暂时忽略其中的部分内容,重点将会放在虚拟机栈,即栈帧的介绍中。
浅析运行时数据区各个功能区的作用
1. 堆(Heap)
堆是JVM中最大的一块内存区域,主要用于存储所有类实例(对象)和数组。
2. 方法区(Method Area)
方法区是所有线程共享的内存区域,用于存储每个类的结构如运行时常量池、字段和方法数据、构造函数和普通方法的字节码内容。它类似于永久代,但是随着时间的推移,它已经和永久代分开,并且可以是堆的一个逻辑部分,也可以是虚拟机自己的内存(非堆)。
3. 虚拟机栈(Stack)
Java虚拟机的栈是一组私有的执行栈,每个线程有自己的执行栈。每个方法执行的同时都会创建一个栈帧用于存储局部变量表, 操作数栈, 动态链接, 方法返回等信息。一个 Java 方法从调用到执行完的过程, 就对应着一个栈帧从虚拟机栈入栈到出栈的过程。
4. 程序计数器(Program Counter Register)
程序计数器是一块较小的内存空间,它存储当前线程正在执行的字节码的地址,即当前指令的地址。
5. 本地方法栈(Native Method Stack)
本地方法栈用于存储本地方法(如用C或C++编写的方法)的局部变量和参数。它与Java栈类似,其区别只是虚拟机栈为 JVM 执行 Java 方法服务, 而本地方法栈则是为虚拟机使用到的本地 (Native) 方法服务。
栈帧
还记得我们刚刚略过的i++吗,现在回到正题,我们都知道i++与++i的机制:
i++
:首先返回i
的当前值,然后i
的值加1。++i
:先将i
的值加1,然后返回新值。
为了理解i++
和++i
在JVM层面的实现,我们需要了解JVM栈帧的结构。当一个方法被调用时,JVM为这个方法创建一个栈帧,它包含了以下关键部分:
- 局部变量表
- 操作数栈
- 动态链接
- 方法返回地址
- 附加信息
我们先简单从字面意义上了解一下前两个部分的作用。
局部变量表:主要用于存储方法参数和定义在方法体内的局部变量。
操作数栈:主要用于保存计算过程的中间结果,同时作为计算过程中变量临时的存储空间。相当于一个工作区。
以下内容配图观看(以i++为例):
而后压一个1进入操作数栈中,执行加一操作。
最后将i值赋值回局部变量表中。
i++
对于后缀递增操作,JVM需要先提供变量i
的当前值,然后再将其值增加一。在栈帧层面,这通常意味着以下步骤:
- 加载变量:将变量
i
的值加载到操作数栈上。 - 返回当前值:由于需要返回当前值,因此这一步加载的值就是表达式
i++
的值。 - 增加一:将一个常量1推送到操作数栈上,然后执行
iadd
(整数加法)指令,将i
的值与1相加。 - 存储结果:将加法的结果存储回局部变量表中的
i
。
在字节码层面,可能是这样:
iload_0 // 加载局部变量i到栈上
dup // 复制栈顶的值(i的当前值)
iconst_1 // 将常量1压入栈
iadd // 执行加法操作,栈顶的两个整数相加
istore_0 // 将相加的结果存储回局部变量i
++i
对于前缀递增操作,JVM首先将i
的值增加一,然后再提供新值。这在字节码层面的实现通常如下:
- 加载变量:将变量
i
的值加载到操作数栈上。 - 增加一:将一个常量1推送到操作数栈上,然后执行
iadd
指令,将i
的值与1相加。 - 存储结果:将加法的结果存储回局部变量表中的
i
。 - 返回新值:此时,操作数栈上的值已经是增加后的值,可以直接使用。
对应的字节码可能如下:
iload_0 // 加载局部变量i到栈上
iconst_1 // 将常量1压入栈
iadd // 执行加法操作
istore_0 // 将相加的结果存储回局部变量i
Java栈帧
小栗子(finally)
我们再来看一个小栗子:
不是这个,是这个:
注:这段代码示例借鉴自:
【Java】异常基本知识点-CSDN博客
我们知道try-catch-finally模板主要用于异常的抓取,如果try块中识别到异常,则执行catch块中的代码,而无论有无异常,都会执行finally块。
这段代码首先将10传入number方法中,而后进入try语句块,发现并未捕捉到异常,正想要返回,却发现还有个finally语句,于是跳转至finally语句,执行两行语句之后,欣然返回main方法。
目前看来,执行的顺序大概是这样:
这时问题出现了,最后print出的num应该是多少?如果从执行顺序来看,似乎应该是11,毕竟是先执行的++num,再运行的return num。
不卖关子,直接运行。
这是为什么呢?疑惑龙.jpg:
掏出我们刚学的栈帧,来看看这段代码:
main
方法的栈帧
- 方法调用:
main
方法调用number(10)
方法。 - 创建栈帧:JVM为
number
方法创建一个新的栈帧。 - 参数传递:
number
方法的参数num
被设置为10,并存储在新栈帧的局部变量表中。
number
方法的栈帧
- 局部变量表:
num
的初始值设置为10。 - 操作数栈:
System.out.println("开始")
执行,将字符串"开始"推送到操作数栈上,随后调用系统输出方法。 - 返回值:
return num;
执行,num
的值(10)被推送到操作数栈上,准备返回给main
方法。
catch
块
尽管number
方法中有一个try-catch
语句,但在这个例子中,并没有抛出NumberFormatException
,因此catch
块不会被执行。
finally
块
- 执行
finally
:无论是否发生异常,finally
块中的代码都会执行。 - 局部变量更新:
++num;
执行,num
的值从10增加到11,但这个改变只在number
方法的栈帧中有效。
返回main
方法
- 方法返回:
number
方法返回,操作数栈上的返回值(10)传递给main
方法。 - 输出结果:
main
方法中的System.out.println
使用这个返回值打印"num = 10"。 finally
块完成:尽管main
方法已经接收到返回值,但number
方法的finally
块仍然会执行,输出"运行完成"。
正式介绍一下栈帧
由于笔者才疏学浅,对动态链接具体的机制不甚清楚,故而放到日后的博客中详细记述。
把上面的栈帧结构搬过来~
- 局部变量表(Local Variables)
- 操作数栈(Operand Stack)
- 动态链接(Dynamic Linking)
- 方法返回地址(Return Address)
- 附加信息
局部变量表
局部变量表也被称为局部变量数组或本地变量表。
定义为一个数字数组,主要用于存储方法参数和定义在方法体内的局部变量。
操作数栈
操作数栈,也可称为表达式栈,它是存放字节码操作指令的栈,存在于每一个独立的栈帧中。
在方法执行过程中,根据字节码指令,往栈中写入数据或提取数据,即入栈(push)/出栈(pop)。
某些字节码指令将值压入操作数栈,其余的字节码指令将操作数取出栈。使用它们之后再把结果压入栈。
比如:执行复制、交换、求和等操作。
注:
· 操作数栈,主要用于保存计算过程的中间结果,同时作为计算过程中变量临时的存储空间。
· 操作数栈就是JVM执行引擎的一个工作区,当一个方法刚开始执行的时候,一个新的栈帧也会随之被创建出来,这个方法的操作数栈是空的。
· 操作数栈并非采用访问索引的方式来进行数据访问,而是只能通过标准的入栈和出栈操作来完成一次数据访问。
动态链接
每一个栈帧内部都包含一个指向运行时常量池中该栈帧所属方法的引用。包含这个引用的目的就是为了支持当前方法的代码能够实现动态链接。
在Java源文件被编译到字节码文件中时,所有的变量和方法引用都作为符号引用(Symbolic Reference)保存在class文件的常量池里。
比如:描述一个方法调用了另外的其他方法时,就是通过常量池中指向方法的符号引用来表示的,那么动态链接的作用就是为了将这些符号引用转换为调用方法的直接引用。
方法返回地址
存储下一条指令的地址:当一个方法被调用时,JVM会将调用下一条指令的地址压入调用者方法的栈帧中。这个地址是方法调用后应该继续执行的地方。
方法返回时恢复执行:当方法执行完毕并准备返回时,JVM会从当前栈帧中弹出返回地址,并将其放入程序计数器(Program Counter Register)中。这样,当控制权从当前方法返回到调用者方法时,JVM就知道接下来应该执行哪一条指令。
方法返回的两种方式:
1)执行引擎遇到任意一个方法返回的指令(return),会有返回值传递给上层的方法调用者,简称正常完成出口。
2)在方法执行的过程中遇到了异常(Exception),如果有异常处理器,则交给异常处理器,如果无,则抛出异常简称异常完成出口。
结语
笔者对栈帧,对JVM的理解仍有许多不足之处,光是落笔的当下,就会生出许多问题,例如:动态链接机制是如何实现的?是否存在静态链接?GC垃圾回收机制如何实现,何为新生代和老生代?期待在日后的学习中,能自己一一解决。
参考资料:
一起来边打扫卫生边学习JVM运行时数据区_哔哩哔哩_bilibili
JVM-用栈帧来炼制i++丹药_哔哩哔哩_bilibili
Java高级面试题:栈帧结构以及动态链接是什么?_哔哩哔哩_bilibili