本文共 2674 字,大约阅读时间需要 8 分钟。
每个栈帧中存储着:
- 局部变量表(Local Variables);
- 操作数栈(Operand Stack)(或表达式栈);
- 动态链接(Dynamic Linking)(或指向运行时常量池的方法引用);
- 方法返回地址(Return Address)(或方法正常退出或者异常退出的定义);
- 一些附加信息;
1.局部变量表(Local Variables)
- 局部变量表也被称为局部变量数组或本地变量表;
- 定义为一个数字数组,主要用于存储方法参数和定义在方法体内的局部变量,这些数据类型包括各类基本数据类型、对象引用(reference),以及returnAddress(返回值)类型。
- 由于局部变量表是建立在线程的栈上,是线程的私有数据,因此不存在数据安全问题;
- 局部变量表所需的容量大小是在编译期确定下来的,并保存在方法的Code属性的maximum local variables数据项中。在方法运行期间是不会改变局部变量表的大小的。
- 方法嵌套调用的次数由栈的大小决定。一般来说,栈越大,方法嵌套调用次数越多。对一个函数而言,它的参数和局部变量越多,使得局部变量表膨胀,它的栈帧就越大,以满足方法调用所需传递的信息增大的需求。进而函数调用就会占用更多的栈空间,导致其嵌套调用次数就会减少。
- 局部变量表中的变量只在当前方法调用中有效。在方法执行时,虚拟机通过使用局部变量表完成参数值到参数变量列表的传递过程。当方法调用结束后,随着方法栈帧的销毁,局部变量表也会随之销毁。
关于Slot(变量槽)的理解
- 参数值的存放总是在局部变量数组的index0开始,到数组长度-1的索引结束。
- 局部变量表,最基本的存储单元是Slot(变量槽)
- 局部变量表中存放编译期可知的各种基本数据类型(8种),引用类型(reference),returnAddress(返回值)类型;
- 在局部变量表里,32位以内的类型只占用一个Slot(包括returnAddress类型),64位的类型(long和double)占用两个slot;
- byte、short、char、long、float在存储前被转换为int,Boolean也被转换为int,0表示false,非0表示true;
- double则占据两个Slot;
字节码中方法内部结构的剖析
图一:
图二:
图三:
图四:
图五:
- 我们看向图五,在变量槽索引为0的位置,这里有一个this变量,这就是为什么静态方法中不能使用this的原因,因为this变量不存在于静态方法的局部变量表中;
(Slot)变量槽的重复利用
类变量和局部变量的对比
2.操作数栈(Operand Stack)
- 如果被调用的方法带有返回值的话,其返回值将会被压入当前栈帧的操作数栈中,并更新PC寄存器中下一条需要执行的字节码指令。
- 操作数栈中元素的数据类型必须与字节码指令的序列严格匹配,这由编译器在编译器期间进行验证,同时在类加载过程中的类效验阶段的数据流分析阶段要再次验证。
- 另外,我们说Java虚拟机的解释引擎是基于栈的执行引擎,其中的栈指的就是操作数栈;
- 操作数栈可理解为java虚拟机栈中的一个用于计算的临时数据存储区
操作数栈的执行原理
- PC寄存器获取字节码指令
- 操作数栈压栈(入栈)
- 通过执行引擎进行运算操作
- 弹(出)栈推出结果储存至局部变量表中
字节码指令执行示例:
iload_0 //将局部变量0中的int压入堆栈iload_1 //将局部变量1中的int压入堆栈iadd //弹出两个整数,添加它们,然后推入结果istore_2 // pop int,存储到局部变量2中
3.动态链接(Dynamic Linking 指向运行时常量池的方法引用)
- 每一个栈帧内部都包含一个指向运行时常量池中该栈帧所属方法的引用。包含这个引用的目的就是为了支持当前方法的代码能够实现动态链接(Dynamic Linking)。比如:invokedynamic指令;
- 在Java源文件被编译到字节码文件中,所有的变量和方法引用都作为符号引用(Symbolic Reference)保存在class文件的常量池里。比如:描述一个方法调用了另外的其他方法时,就是通过常量池中指向方法的符号引用来表示的,那么动态链接的作用就是为了将这些符号引用转换为调用方法的直接引用;
为什么需要常量池呢?
常量池的作用:就是为了提供一些符号和常量,便于指令的识别;
方法的调用
虚方法和非虚方法
- 如果方法在编译期就确定了具体的调用版本,这个版本在运行时是不可变的,这样的方法称为非虚方法。
- 静态方法、私有方法。final方法、实例构造器、父类方法都是非虚方法;
- 其他方法称为虚方法。
方法重写的本质
虚方法表
例如:
-
方法表会在类的连接阶段初始化,方法表存储的是该类方法入口的一个映射,比如父类的方法A的索引号是1,方法B的索引号是2…
如果子类继承了父类,但是某个父类的方法没有被子类重写,那么在子类的方法表里边该方法指向的是父类的方法的入口,子类并不会重新生成一个方法,然后让方法表去指向这个生成的,这样做是没有意义的。还有一点,如果子类重写了父类的方法,那么子类这个被重写的方法的索引和父类的该方法的索引是一致。比如父类
A的test方法被子类C重写了,那么子类C的test方法的索引和父类A的test方法的索引都是1(打个比方),这样做的目的是为了快速查找,比如说在子类里边找不到一个方法索引为1的方法,那么jvm会直接去父类查找方法索引为1的方法,不需要重新在父类里边遍历。
4.方法返回地址(return address)
当一个方法开始执行后,只有两种方式可以退出这个方法:
1、执行引擎遇到任意一个方法返回的字节码指令(return),会有返回值传递给上层的方法调用者,简称正常完成出口;
- 一个方法在正常调用完成之后究竟需要使用哪一个返回指令还需要根据方法返回值的实际数据类型而定。
- 在字节码指令中,返回指令包含ireturn(当返回值是boolean、byte、char、short和int类型时使用)、lreturn、freturn、dreturn以及areturn,另外还有一个return指令供声明为void方法、实例初始化方法、类和接口的初始化方法使用。
2、在方法中遇到异常(Expection),并且这个异常没有在方法内进行处理,也就是只要在本地方法的异常表中没有搜索到匹配的异常处理器,就会导致方法退出。简称异常完成出口
转载地址:http://jqqzi.baihongyu.com/