Java虚拟机以方法作为最基本的执行单元,“栈帧”(Stack Frame)则是用于支持虚拟机进行方法调用和方法执行背后的数据结构,它也是虚拟机运行时数据区中的虚拟机栈(Virtual Machine Stack)的栈元素。
每一个栈帧都包括了局部变量表、操作数栈、动态连接、方法返回地址和一些额外的附加信息。 如下图(栈帧的概念结构)所示:
1. 局部变量表(重要)
局部变量表(Local Variables Table)是一组变量值的存储空间,用于存放方法参数和方法内部定义的局部变量。在Java程序被编译为Class文件时,就在方法的Code属性的max_locals数据项中确定了该方法所需分配的局部变量表的最大容量。
局部变量表的容量以变量槽(Variable Slot)为最小单位,《Java虚拟机规范》中并没有明确指出一个变量槽应占用的内存空间大小。一个变量槽可以存放一个32位以内的数据类型,Java中占用不超过32位存储空间的数据类型有 boolean、byte、char、short、int、 float、reference 和 returnAddress 这8种类型。
第7种reference类型表示对一个对象实例的引用,《Java虚拟机规范》既没有说明它的长度,也没有明确指出这种引用应有怎样的结构。
第8种returnAddress类型目前已经很少见了,它是为字节码指令jsr、jsr_w和ret服务的,指向了一条字节码指令的地址,某些很古老的Java虚拟机曾经使用这几条指令来实现异常处理时的跳转,但现在也已经全部改为采用异常表来代替了。
对于64位的数据类型,Java虚拟机会以高位对齐的方式为其分配两个连续的变量槽空间。Java语言中明确的64位的数据类型只有 long 和 double 两种。
这里把long和double数据类型分割存储的做法与“long和double的非原子性协定”中允许把一次long和double数据类型读写分割为两次32位读写的做法有些类似,读者阅读到本书关于Java内存模型的内容时可以进行对比。不过,由于局部变量表是建立在线程堆栈中的,属于线程私有的数据,无论读写两个连续的变量槽是否为原子操作,都不会引起数据竞争和线程安全问题。
2. 操作数栈(重要)
操作数栈(Operand Stack)也常被称为操作栈,它是一个后入先出(Last In First Out,LIFO)栈。操作数栈的每一个元素都可以是包括long和double在内的任意Java数据类型。32位数据类型所占的栈容量为1,64位数据类型所占的栈容量为2。
这里列举一个局部变量表和操作数栈交互的一个案例:
分析题:a++ + ++a的执行结果,案例代码如下:
/** * 从字节码角度分析 a++ 相关题目 */ public class Demo3_2 { public static void main(String[] args) { int a = 10; int b = a++ + ++a + a--; System.out.println(a);// 11 System.out.println(b);// 34 } }
上面a、b的结果是怎样得来的呢?
分析:
iinc 指令是直接在局部变量桶位(slot)上进行运算。
iload指令是用于读取变量
a++ 和 ++a 的区别是先执行 iload还是 先执行iinc。a++是先 iload再iinc,++a相反。
对虚拟机指令不清楚的去看一下这篇文章:JVM_07 类加载与字节码技术(字节码指令)
①bipush 10 操作是把a = 10 放入操作数栈:
② istore 1
操作,把操作数栈中的10弹出,放入到局部变量表的槽位1中:
③ 接下来执行a++
操作,我们上边提前说明了,a++
是先执行iload
读取,再执行iinc
加 1
-
iload 1
将 变量a=10
,读取到操作数栈stack中: -
执行
iinc
指令,在局部变量表上对a进行 +1 操作,这时候 a 为11: - ④ 下面执行
++a
操作,先iinc
在iload
: - 执行
iinc
指令,在局部变量表上对a进行+1操作,这时候a为12: -
iload 1
将局部变量表中a=12
,读取到操作数栈stack中: -
⑤ 下面进行
a++ + ++a
操作,在操作数栈中进行相加,得到结果22,这时候第1个加法完成: - ⑥ 下面执行第二个加法
(a++ + ++a)+ a--
操作: -
a--
先执行iload
命令,在执行inc 1,-1
命令,如下,先将局部变量表中的12读取到操作数栈: -
接下来执行
inc 1,-1
命令,在局部变量表中进行-1操作,此时局部变量表中的值由12减为11: - 在操作数栈中,执行第二次加法运算,得到结果为34:
- ⑦ 最后将操作数栈中的数据弹出到局部变量表中,赋值2号槽位b=34:
因此程序运行结果得到:a为11,b为34。
3. 动态连接(了解)
每个栈帧都包含一个指向运行时常量池[1]中该栈帧所属方法的引用,持有这个引用是为了支持方法调用过程中的动态连接(Dynamic Linking)。
我们知道Class文件的常量池中存有大量的符号引用,字节码中的方法调用指令就以常量池里指向方法的符号引用作为参数。这些符号引用一部分会在类加载阶段或者第一次使用的时候就被转化为直接引用,这种转化被称为静态解析。另外一部分将在每一次运行期间都转化为直接引用,这部分就称为动态连接。
4. 方法返回地址(了解)
当一个方法开始执行后,只有两种方式退出这个方法。第一种方式是执行引擎遇到任意一个方法返回的字节码指令,这时候可能会有返回值传递给上层的方法调用者(调用当前方法的方法称为调用者或者主调方法),方法是否有返回值以及返回值的类型将根据遇到何种方法返回指令来决定,这种退出方法的方式称为“正常调用完成”(Normal Method Invocation Completion)。
另外一种退出方式是在方法执行的过程中遇到了异常,并且这个异常没有在方法体内得到妥善处理。无论是Java虚拟机内部产生的异常,还是代码中使用athrow字节码指令产生的异常,只要在本方法的异常表中没有搜索到匹配的异常处理器,就会导致方法退出,这种退出方法的方式称为“异常调用完成(Abrupt Method Invocation Completion)”。一个方法使用异常完成出口的方式退出,是不会给它的上层调用者提供任何返回值的。
无论采用何种退出方式,在方法退出之后,都必须返回到最初方法被调用时的位置,程序才能继续执行,方法返回时可能需要在栈帧中保存一些信息,用来帮助恢复它的上层主调方法的执行状态。一般来说,方法正常退出时,主调方法的PC计数器的值就可以作为返回地址,栈帧中很可能会保存这个计数器值。而方法异常退出时,返回地址是要通过异常处理器表来确定的,栈帧中就一般不会保存这部分信息。
方法退出的过程实际上等同于把当前栈帧出栈,因此退出时可能执行的操作有:恢复上层方法的局部变量表和操作数栈,把返回值(如果有的话)压入调用者栈帧的操作数栈中,调整PC计数器的值以指向方法调用指令后面的一条指令等。
5. 附加信息(了解)
《Java虚拟机规范》允许虚拟机实现增加一些规范里没有描述的信息到栈帧之中,例如与调试、性能收集相关的信息,这部分信息完全取决于具体的虚拟机实现,这里不再详述。在讨论概念时,一般会把动态连接、方法返回地址与其他附加信息全部归为一类,称为栈帧信息。
结语:
非常建议学习Java的小伙伴,买一本周志明老师的《深入理解Java虚拟机(第3版)》去读一读,博客和视频教程,始终不如看书来得实在呀!
后续会陆续更新,这本书的笔记记的差不多了,排版和格式需要花时间整理,文章都会同步到公众号上,也欢迎大家通过公众号加入我的交流qun互相讨论jvm这块的知识内容!