栈帧
-
虚拟机栈中存储的是一个个栈帧,每个栈帧代表一个方法体,一个栈帧的进栈与出栈对应着一个方法的调用开始和结束。
-
每一个栈帧都包含局部变量表、操作数栈、动态链接、方法返回地址和一些额外的附加信息,局部变量表和操作数栈所需要多大空间已经在编译器确定下来;
-
执行引擎只会处理位于栈顶的栈帧,这个栈帧称为当前栈帧。
局部变量表
- 存放方法参数和方法内的局部变量
- 最小单位为Slot,除了long和double数据类型变量占用2个Slot,其他数据类型占用1个Slot
- 当前方法为实例方法时(非static方法),局部变量表的第0位Slot存储方法所属对象实例的应用“this”
- 变量表顺序——“this”->参数表->方法体内局部变量
操作数栈
- 32位数据类型占用1个栈容量,64位占用2个
- 优化:让下面栈帧的部分操作数栈与上面栈帧的部分局部变量表重叠,方法调用时共用一些数据,减少额外的参数复制传递
动态连接
- 指向运行时常量池中该栈帧所属方法的引用
方法返回地址
返回到方法被调用的位置,恢复上层方法的局部变量表和操作数栈,将返回值(有的话)压如调用者栈帧的操作数栈中,调整PC计数器指向方法调用指令后面的一条指令。
- 正常退出
- 异常退出,匹配方法的异常表
方法调用
方法调用阶段只是确定被调用方法的版本,就是要调用哪一个方法。
- 解析
- “编译期可知,运行期不可变”的方法,只会有一个版本的方法:静态方法、私有方法、父类方法、实例构造器方法、final方法。这几个方法在类加载阶段进行了静态分析,将符号引用转化为了直接引用。
- 其他方法称为“虚方法”,将会根据运行期的不同进行不懂解析,称为动态绑定。
- 分派–动态绑定
- Java多态性的体现
- 静态分派(重载)
- 编译器对静态类型可知,对实际类型不可知,运行时才对实际类型可知
- Human man = new Man();Human是静态类型,Man是实际类型,而sr.sayHello(man)中,编译器可知man的静态类型为Human,而不知实际类型Man;
- 虚拟机(其实是编译器)在重载时通过参数的静态类型而不是实际类型作为判断依据的
- 动态分派(重写)
- invokevirtual指令在运行时的解析动作
- 1)找到操作数栈顶的第一个元素所指向的对象的实际类型,计做C;
- 2)检查C的合法性
- 虚拟机在重写是通过实际类型决定方法的版本
- Human man = new Man(); man.sayHello();
- 执行的是实际类型Man中的方法。
- invokevirtual指令在运行时的解析动作
基于栈的指令集与基于寄存器的指令集
- 基于栈的指令集,
- 优点:可移植(一次编写,到处运行),代码紧凑
- 缺点:执行速度慢,指令数量多,内存访问多
- 完成相同的功能,栈架构所需要的指令数一般多于寄存器架构,栈实现在内存中,频繁的栈访问意味着频繁的内存访问,这将降低代码的执行速度。