Java JVM 栈帧(Stack Frame)
一、概述
栈帧位置
JVM 执行 Java 程序时需要装载各种数据到内存中,不同的数据存放在不同的内存区中(逻辑上),这些数据内存区称作运行时数据区(Run-Time Data Areas)。
其中 JVM Stack(Stack 或虚拟机栈、线程栈、栈)中存放的就是 Stack Frame(Frame 或栈帧、方法栈)。
对应关系
一个线程对应一个 JVM Stack。JVM Stack 中包含一组 Stack Frame。线程每调用一个方法就对应着 JVM Stack 中 Stack Frame 的入栈,方法执行完毕或者异常终止对应着出栈(销毁)。
当 JVM 调用一个 Java 方法时,它从对应类的类型信息中得到此方法的局部变量区和操作数栈的大小,并据此分配栈帧内存,然后压入 JVM 栈中。
在活动线程中,只有位于栈顶的栈帧才是有效的,称为当前栈帧,与这个栈帧相关联的方法称为当前方法。
栈帧结构
一个栈帧需要分配多少内存,不会受到程序运行期变量数据的影响,而仅仅取决于具体的虚拟机实现。
局部变量表(Local Variable Table)
- 在编译程序代码的时候就可以确定栈帧中需要多大的局部变量表,具体大小可在编译后的 Class 文件中看到。
- 局部变量表的容量以 Variable Slot(变量槽)为最小单位,每个变量槽都可以存储 32 位长度的内存空间。
- 在方法执行时,虚拟机使用局部变量表完成参数值到参数变量列表的传递过程的,如果执行的是实例方法,那局部变量表中第 0 位索引的 Slot 默认是用于传递方法所属对象实例的引用(在方法中可以通过关键字 this 来访问到这个隐含的参数)。
- 其余参数则按照参数表顺序排列,占用从 1 开始的局部变量 Slot。
- 基本类型数据以及引用和 returnAddress(返回地址)占用一个变量槽,long 和 double 需要两个。
操作数栈(Operand Stack)
- 同样也可以在编译期确定大小。
- Frame 被创建时,操作栈是空的。操作栈的每个项可以存放 JVM 的各种类型数据,其中 long 和 double 类型(64位数据)占用两个栈深。
- 方法执行的过程中,会有各种字节码指令往操作数栈中写入和提取内容,也就是出栈和入栈操作(与 Java 栈中栈帧操作类似)。
- 操作栈调用其它有返回结果的方法时,会把结果 push 到栈上(通过操作数栈来进行参数传递)。
动态链接(Dynamic Linking)
- 每个栈帧都包含一个指向运行时常量池中该栈帧所属方法的引用,持有这个引用是为了支持方法调用过程中的动态链接。
- 在类加载阶段中的解析阶段会将符号引用转为直接引用,这种转化也称为静态解析。另外的一部分将在运行时转化为直接引用,这部分称为动态链接。
返回地址(Return Address)
- 方法开始执行后,只有 2 种方式可以退出 :方法返回指令,异常退出。
帧数据区(Stack Data)
- 帧数据区的大小依赖于 JVM 的具体实现。
二、反编译代码
源代码
package com.jvm; /** * 编译:javac com\jvm\StackFrame.java * 反编译:javap -p -v com\jvm\StackFrame.class */ public class StackFrame { public static void main(String[] args) { add(1, 2); } private static int add(int a, int b) { int c = 0; c = a + b; return c; } }
反编译后的字节码(去除了不相关字节码)
{ public com.jvm.StackFrame(); descriptor: ()V flags: ACC_PUBLIC Code: stack=1, locals=1, args_size=1 0: aload_0 1: invokespecial #1 // Method java/lang/Object."<init>":()V 4: return LineNumberTable: line 7: 0 public static void main(java.lang.String[]); descriptor: ([Ljava/lang/String;)V flags: ACC_PUBLIC, ACC_STATIC Code: stack=2, locals=1, args_size=1 0: iconst_1 1: iconst_2 2: invokestatic #2 // Method add:(II)I 5: pop 6: return LineNumberTable: line 9: 0 line 10: 6 private static int add(int, int); descriptor: (II)I flags: ACC_PRIVATE, ACC_STATIC Code: stack=2, locals=3, args_size=2 0: iconst_0 1: istore_2 2: iload_0 3: iload_1 4: iadd 5: istore_2 6: iload_2 7: ireturn LineNumberTable: line 13: 0 line 14: 2 line 15: 6 }
三、字节码解释
主要看 add 方法
# 方法描述 # 括号内为入数类型,这里为两个 int 型入参 # 括号外为返回类型,这里为返回 int 型 descriptor: (II)I # 方法类型,这里为私有的静态方法 flags: ACC_PRIVATE, ACC_STATIC # 操作数栈为 2 # 本地变量容量为 3 # 入参个数为 2 stack=2, locals=3, args_size=2
执行 add(1,2) 的过程,最后 ireturn 会将操作数栈栈顶的值返回给调用者。注意这里 add 是静态方法,其局部变量表中没有 this。
LineNumberTable 为代码行号与字节码行号的对应关系
https://blog.csdn.net/qian520ao/article/details/79118474
https://docs.oracle.com/javase/specs/jvms/se8/html/jvms-2.html#jvms-2.5