0x00 运行时栈帧结构
每个方法从调用开始至结束,都对应一个站帧在虚拟机入栈出栈的过程。
- 局部变量表
- 操作数栈
- 动态链接
- 返回地址
- 附加信息
局部变量表
局部变量表是一组变量存储空间,存储方法参数和局部变量。Code属性中的max_locals确定了它的最大容量。
- 局部变量表容量以变量槽(Slot)为基本单位。Slot可以复用。
- 除了long和double使用两个Slot外,其他都使用1个。
- 如果为实例方法,第0位为this。
- 局部变量不存在准备阶段,变量必须赋予初始值,不然虚拟机不接受。
操作数栈
操作数栈是一个LIFO栈,max_stacks确定栈的深度。
- 操作数栈的元素类型必须与字节码指令严格匹配。
- 在编译阶段由编译器保证。
- 在类校验阶段再次验证。
动态链接
每个栈帧都有一个指向运行时常量池中该栈帧所属方法的引用。
返回地址
只有两种方式退出方法。
- 方法执行过程中遇到返回的字节码指令。
- 方法执行过程中遇到一次,并且在方法中没有相应的处理。(该方式不产生返回值)
常见的方法退出操作:1恢复上层方法的局部变量表和操作数栈,2将返回值压入上层栈中,3PC计数器执行下一条指令。
一般把动态链接、返回地址、附加信息统称为栈帧信息。
方法调用
方法调用阶段的目的只有一个:确定被调用方法的版本,不涉及方法内部的具体运行过程。
0x00 解析
在类加载的解析阶段,会将
一部分
符号引用转换为直接引用,前提是方法在运行之前有一个确定的调用版本,且这个方法的调用版本在运行期间不可变。这类方法的调用称为解析(Resolution)。编译期间可知,运行期间不可变的方法,主要包括静态方法和私有方法。它们适合在类加载阶段进行解析。
- invokestatic:调用静态方法。
- invokespecial调用实例构造器init方法、私有方法、父类方法。
- invokevirtual:调用所有虚方法。
- invokeinterface:调用接口方法。在运行时在确定实现该接口的对象。
- invokedynamic:在运行时动态解析调用点限定符所引用的方法,然后再执行该方法。
只要能被invokestatic和invokespecial调用的方法,都可以在解析阶段确定调用版本,符合的有静态方法、私有方法、构造器方法、父类方法4中。它们在类加载时将符号引用解析为直接引用,这些方法称为非虚方法
,以及一个final修饰的方法,虽然final修饰的方法编译出来的指令为invokevirtual,但是虚拟机明确规定final方法是一种非虚方法。
分派
分派(Dispatch)调用可能为静态调用,也可能为动态调用,依据宗量数可分为单分派很多分派。
静态分派
Human x = new Man(); Human称静态类型(static type),Man称为实际类型(actual type)。
- 静态类型的变化只在使用中出现,变量本身的静态类型不会被改变,且最终静态类型编译期间可知。
- 实际类型变化的结果在运行期间才可确定。
- 编译器在重载是通过参数的静态类型判定。
- 所有依赖静态类型定位方法执行版本的分派称为静态分派,典型的有
重载
。 - 静态分派发生在编译阶段。
- 字面量没有显式的静态类型。
动态分派
重写是动态分派的一种体现,使用的指令时invokevirtual。
invokevirtual指令的多态查找步骤:
- 找到操作数栈顶第一个元素指向的对象的实际类型C。
- 如果C于常量中的描述符和简单名称相符,且访问权限通过,查找结束。
- 否则,继续下一个变量。
- 始终没找到,抛出AbstractMethodError。
单分派、多分派
方法的接收者和参数统称宗量。Java是一门静态多分派、动态单分派的语言。