前言
在JVM中,每个线程都包含n个栈帧,每一个栈帧都包括了局部变量表、操作数栈、动态连接、方法返回地址和一些额外的附加信息。
栈帧的生命周期随着方法的创建而创建,随着方法的结束而销毁,无论方法是正常完成还是异常完成(抛出了在方法内未被捕获的异常)都算方法的结束。
在某条线程执行过程中的某个时间点上,只有目前正在执行的那个方法的栈帧是活动的。这个栈帧称为当前栈帧,这个栈帧对应的方法称为当前方法,定义这个方法的类称为当前类。对局部变量表和操作数栈的各种操作,通常都指的是对当前栈帧的局部变量表和操作数栈所进行的操作。
注意: 栈帧是线程本地私有的数据,不可能在一个栈帧 之中引用另外一个线程的栈帧
局部变量表
局部变量表(Local Variables Table)
是一组变量值的存储空间,用于存放方法参数和方法内部定义的局部变量。
存储方法
局部变量表的容量以变量槽(Variable Slot)
为最小单位,一般在虚拟机中,一个Slot占用32位存储空间(这不是固定的,虚拟机可以自行改变每个槽占用空间的大小,但一般都是32位)。
Java虚拟机通过索引定位的方式使用局部变量表,索引值的范围是从0
开始至局部变量表最大的变量槽数量。如果访问的是32位数据类型的变量,索引N就代表了使用第N个变量槽,如果访问的是64位数据类型的变量,则说明会同时使用第N和N+1两个变量槽。
eg:
在Java中,long
在内存占64位,所以局部变量表用2个slot来存储
对于两个相邻的共同存放一个64位数据的两个变量槽,虚拟机不允许采用任何方式单独访问其中的某一个,《Java虚拟机规范》中明确要求了如果遇到进行这种操作的字节码序列,虚拟机就应该在类加载的校验阶段中抛出异常。
long和double的非原子性协定
在Java内存模型中,对于64位的数据类型(long和double),在模型中特别定义了一条宽松的规定:允许虚拟机将没有被volatile修饰的64位数据的读写操作划分为两次32位的操作来进行,即允许虚拟机实现自行选择是否要保证64位数据类型的load、store、read和write这四个操作的原子性,这就是所谓的 “long和double的非原子性协定”(Non-Atomic Treatment of doubleand long Variables) 。
虽然有这个协定,但是,由于局部变量表(Local Variable Table)
是建立在线程堆栈中的,属于线程私有的数据,无论读写两个连续的变量槽是否为原子操作,都不会引起数据竞争和线程安全问题。
初始值问题
我们已经知道类的字段变量有两次赋初始值的过程,一次在准备阶段,赋予系统初始值;另外一次在初始化阶段,赋予程序员定义的初始值。
但局部变量就不一样了,如果一个局部变量定义了但没有赋初始值,那它是完全不能使用的。所以不要认为Java中任何情况下都存在诸如整型变量默认为0、布尔型变量默认为false等这样的默认值规则。
eg:
// 这个方法会报: // Error:(12, 28) java: variable y might not have been initialized public class JVMTest { public static void main(String[] args) { int y; int z=3; System.out.println(y+z); } } // 这个会正常输出 3; 因为int的初始值为0 public class JVMTest { private static int y; public static void main(String[] args) { int z=3; System.out.println(y+z); } }
操作数栈
操作数栈(Operand Stack)
也常被称为操作栈
,它是一个后入先出(Last In First Out,LIFO)栈。同局部变量表一样,操作数栈的最大深度也在编译的时候被写入到Code属性的max_stacks
数据项之中。操作数栈的每一个元素都可以是包括long和double在内的任意Java数据类型。32位数据类型所占的栈容量为1,64位数据类型所占的栈容量为2。Javac编译器的数据流分析工作保证了在方法执行的任何时候,操作数栈的深度都不会超过在max_stacks
数据项中设定的最大值。
eg:
public class JVMTest { public static void main(String[] args) { long y=9223372036854775800L; int z=2; long x=y+z; } }
我们用javap -verbose JVMTest
来查看他的class
文件的字节码指令
在操作栈中的流程大致为:
动态链接
每个栈帧都包含一个指向当前方法所在类型的运行时常量池
的引用,持有这个引用是为了支持方法调用
过程中的动态连接(Dynamic Linking)
。在Class
文件里,一个方法若要调用其他方法,或者访问成员变量,则需要通过符号引用(symolic reference)
来表示,动态链接
的作用就是将这些以符号引用
所表示的方法转换为实际方法的直接引用。
什么是符号引用?
通过查看字节码,上面的#7
,#8
,#9
等等都是符号引用,他在class文件里只是个符号,就像你定义一个变量名称一样,变量名只是和字符符号
,并不是真正的指向内存的地址指针。这些符号都指向运行时常量池
的引用。
方法返回地址
Java在调用方法时,只有两种返回方法,一种是正常返回
,一种是异常返回
。
正常返回
正常返回指的就是在执行方法时,中间并没有异常抛出,或者已正确处理抛出的异常,这时就称当前方法正常调用完成
,如果有返回值,就会给他调用者返回一个值,如果没有返回值(void
)就正常返回。
这种场景下,当前栈帧承担着恢复调用者状态的责任,包括恢复调用者的局部变量表和操作数栈,以及正确递增程序计数器,以跳过刚才执行的调用方法指令等。 调用者的代码在被调用方法的返回值压入调用者栈帧的操作数栈后,会正常执行。
异常返回
在调用一些方法时,一些异常没有被正确捕获,就会导致方法终止,此时称方法异常调用完成
,那一定不会有方法返回值返回给其调用者。
无论采用何种退出方式,在方法退出之后,都必须返回到最初方法被调用时的位置,程序才能继续执行,方法返回时可能需要在栈帧中保存一些信息,用来帮助恢复它的上层主调方法的执行状态。
怎么理解这个必须返回到最初方法被调用时的位置呢?
eg:
上面异常是在13行
发生的,但是它并没有停在13行
,而是回到了最初调用它第10行
的位置。