栈帧结构详解

前言

栈帧结构
  Java虚拟机以方法作为基本的执行单位,“栈帧”是用于支持虚拟机进行方法调用和执行的数据结构,每一个方法从调用开始到执行结束,都对应着一个栈帧在虚拟机栈里面从入栈到出栈的过程,栈帧也是虚拟机运行时数据区中虚拟机栈的栈元素。位于栈顶的栈帧被称为“当前栈帧”,其对应的方法称为“当前方法”。
  栈帧中存储了方法的局部变量表、操作数栈、动态连接、方法返回地址和附加信息(例如,调试、性能收集相关的信息,取决于具体的虚拟机实现,是《Java虚拟机规范》中未描述的信息)。


以下对栈帧中的各个部分进行具体的阐述

一、局部变量表

  局部变量表是一组变量值的存储空间,用于存储方法参数和方法内部定义的局部变量。在Java程序被编译为.Class文件时,在方法的Code属性中的max_locals中确定了该方法所需要分配的局部变量表的最大容量。
  在《深入理解Java虚拟机》中,关于局部变量表的叙述内容有很多,我在通读一遍之后仍然感觉有一点晦涩,于是加以整理归纳为以下要点,足以囊括书中对局部变量表的描述。

1、局部变量表以变量槽(Variables Slot)为其最小单位用来表示大小。

2、《Java虚拟机规范》中并没有明确提及变量槽应该占用多大的空间,但是有引导性的指出其大小至少要能存放一个boolean(1位)、byte(8位)、char(16位)、short(16位)、int(32位)、float(32位)、reference、returnAddress类型的数据,其中reference类型没有明确指出其大小,它表示一个对象实例的引用,returnAddress表示指向一条字节码指令的地址。所以一个变量槽可以这样认为,其可以存放一个32位以内的数据类型,但并不是说规定了每个变量槽占用32位的空间,注意区别说法。

3、对于64位的long和double类型来说,Java虚拟机会以高位对齐的方式为其分配两个连续的变量槽空间,因为是自连续的,所以Java虚拟机不允许以任何的方式单独访问其中一个变量槽,否则会抛出异常。这里分配两个连续变量槽,意味着读写的时候是两个变量槽,但是由于局部变量表所在的栈帧其所在的虚拟机栈是线程私有的,所以哪怕读写两个变量槽是不是原子性的,都不会有线程安全问题。

4、Java虚拟机通过索引定位的方式来使用局部变量表,从0到局部变量表最大的变量槽数量,如果是32位的数据类型变量,索引N就代表使用了第N个变量槽,如果是64位,那么说明会同时使用第N和N+1两个变量槽。

5、当一个方法被调用时,Java虚拟机会使用局部变量表来完成实参到形参的传递。如果虚拟机调用的是一个实例方法(非static修饰),那么索引位置0的变量槽默认就是用于传递方法所属对象实例的引用,可以在方法中使用关键字“this”来访问这个隐含的参数。其余参数按照参数表顺序,从索引1开始分配变量槽,剩下的变量槽按照方法体内定义的变量顺序分配。

6、变量槽可以重用以节省栈帧消耗的内存空间,方法体中的定义的变量,其作用域并不一定会覆盖整个方法,如果当前字节码PC计数器的值已经超出某个变量的作用域,那么这个变量对应的变量槽就可以交给其他变量重用。这一段大部分都是深入理解Java虚拟机的原话,我个人通俗的理解就是,PC计数器的值超过某个变量的作用域,意思就是这个变量已经被执行或者使用过了,完成了它的任务,那么变量自然也就不再需要变量槽的空间了,将变量槽的空间给其他变量复用。关于变量槽的复用,书中还有一起其他描述,例如某些情况下变量槽复用会影响垃圾收集行为,有兴趣的朋友可以取翻一翻,个人觉得读一读了解就行了。

7、最后一点大家都耳熟能详,局部变量不能没有初始值,定义的时候必须设置初始值。

二、操作数栈

  操作数栈又叫做操作栈,是一个后入先出的栈。同局部变量表一样,操作数栈的最大深度也在编译的时候被写入到Code属性的max_stacks数据项中。其可以归纳出以下几个要点。

1、操作数栈的每一个元素都可以是包括long和double在内任意的Java数据类型。32位数据类型占的栈容量为1,64位占的栈容量为2。

2、当一个方法刚执行的时候,这个方法的操作数栈是空的,方法执行的过程中,各种字节码指令往操作数栈写入和提取内容,对应着入栈和出栈的操作。例如执行整数相加的操作时,字节码指令iadd(i代表int类型,add表示相加)要求在运行的时候操作数栈中最近接栈顶的两个元素必须是int同类型(操作数栈中元素的数据类型必须与字节码指令的序列严格匹配)且已经存入操作数栈,执行指令的时候,将两个整数出栈(取出),相加后的结果在入栈(存入)。

3、在概念模型中,两个不同栈帧作为不同方法的虚拟机栈的元素,是完全互相独立的。但是大多数虚拟机的实现会有一些优化处理,另两个栈帧出现一部分重叠。让下面栈帧的部分操作数栈与其上面那个栈帧的部分局部变量表重叠在一起,不仅可以节约一部分空间,重要的是方法调用的时候可以直接共用一部分数据,无须进行额外的参数复制传递。
栈帧之间的数据共享

三、动态连接

  每个栈帧都包含一个指向运行时常量池中该栈帧所属方法的引用,持有这个引用是为了方法调用过程中的动态连接。Class文件中的常量池中存有大量的符号引用,字节码中的方法调用指令就是以常量池里指向方法的符号引用作为参数的,这些符号的一部分会在类加载阶段或者第一次使用的时候被转化为直接引用,这种转化被称为静态解析。另一部分在每一次运行期间转化为直接引用,被称为动态连接(因为方法的执行对应着栈帧的入栈出栈,所以存放在栈帧中)

四、方法返回地址

1、;当一个方法开始执行后,只有两种方式退出这个方法。第一种是执行引擎遇到任意的一个方法返回的字节码指令,这个时候可能会有返回值需要返回给方法的上层调用者(主调方法)。是否有返回值及返回何种类型,由具体的方法返回指令决定。这种退出方式被称为“正常调用完成”。

2、另一种退出方式是方法执行过程中遇到异常,并且异常没有在方法体内得到妥善处理。无论是虚拟机内部产生的异常还是代码中使用athrow字节码指令产生的异常,只要在本方法的异常表中没有搜索到匹配的异常处理器,就会导致方法退出,称为“异常调用完成”,其不会给它的上层调用者提供任何返回值。

3、无论何种方式退出,方法退出之后都必须返回最初方法被调用的位置,程序才能继续执行,方法返回时可能需要在栈帧中保存一些信息,用来恢复其上层主调方法的执行状态。正常退出时,主调方法的PC计数器的值就可以作为返回地址,栈帧中很可能(视具体的虚拟机而定)会保存这个计数器值。异常退出时,返回地址需要通过异常处理器表确定,栈帧中一般就不会保存这部分信息。

4、方法退出意味着当前栈帧出栈,因此退出时可能(视具体的虚拟机而定)执行的操作有:恢复上层方法的局部变量表和操作数栈,把返回值压入调用者栈帧的操作数栈,调整PC计数器的值以指向方法调用指令后面的一条指令,等操作。

五、附加信息

  《Java虚拟机规范》允许虚拟机实现增加一些规范里没有描述的信息到栈帧中,例如与调试、性能收集相关的信息,具体信息取决于具体的虚拟机实现。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值