Java虚拟机规范中制定了虚拟机字节码执行引擎的概念模型,使各种不同的虚拟机实现具有相同的行为,即输入的是字节码文件,处理过程是字节码解析,输出的是执行结果。
运行时栈帧结构
栈帧是用于支持虚拟机进行方法调用和方法执行的数据结构,存储了方法的局部变量表、操作数栈、动态连接、方法返回地址等信息,每一个方法从调用开始至执行完成的过程,都会对应一个栈帧在虚拟机栈从入栈到出栈的过程。在编译程序代码的时候栈帧中需要多大的局部变量表、操作数栈都已完全确定,并且写入了方法表的属性中,即一个栈帧的大小不会受到运行期间变量数据的影响,仅取决于虚拟机的实现。
局部变量表
局部变量表用于存放方法参数和方法内部定义的局部变量,局部变量表的容量以变量槽--Slot为最小单位,Slot的大小虚拟机规范没有规定,但要求一个Slot能存放基本数据类型和引用类型。对于引用类型,需要满足两点:1、通过此引用直接或间接找到对象在Java堆中的起始地址索引;2、直接或间接找到对象类型在方法区中的类型信息。
方法执行时,虚拟机使用局部变量表完成从参数值到参数变量列表的传递过程,如果执行的是实例方法(非 static),局部变量表中第0位索引的Slot默认传递方法所属对象实例的引用,也就是this。其余参数按照参数表顺序排列,之后分配方法体内部的局部变量。
操作数栈
当一个方法开始执行的时候,它的操作数栈是空的,之后执行中会有各种字节码指令往操作数栈中写入和提取内容。
操作数栈中元素的数据类型必须与字节码指令的序列严格匹配,这个在编译阶段就会被严格检查。
动态连接
每个栈帧都有一个指向运行时常量池中该栈帧所属方法的引用,这么做是为了支持方法调用过程中的动态连接,也就是多态。
Class文件的常量池中存放了大量的符号引用,字节码中的方法调用指令就以常量池中指向方法的符号引用作为参数,这些符号引用一部分在类加载阶段就转化为直接引用,称为静态解析;另一部分在每一次运行期间转化为直接引用,称为动态连接。
方法调用
Java中方法的调用分为解析调用和分派调用。在Java中符合“编译器可知、运行期不可变”要求的方法主要有两类,静态方法和私有方法,这样就决定了他们不会有多态的特性,因此在类加载阶段就会被解析,也就是解析调用。另一类就是在运行时确定调用的具体是哪一个方法,分派调用。
与之对应的是5条字节码指令
1、invokestatic 调用静态方法
2、invokespecial 调用实例构造器<init>方法、私有方法和父类方法
3、invokevirtual 调用虚方法
4、invokeinterface 调用接口方法,在运行时确定具体的对象
5、invokedynamic 先在运行时动态解析出调用点限定符所引用的方法,然后再执行
解析调用是个静态过程,编译器确定;分配调用可能是静态或动态,还可分单分派和多分派;
虚拟机(编译器)在重载时是通过参数的静态类型而不是实际类型作为判定依据的,静态类型编译器可知。所有依赖静态类型来定位方法执行版本的分派动作称为静态分派,典型应用就是方法重载。下面是一个例子
public class Main {
static abstract class Human {
}
static class Man extends Human {
}
static class Woman extends Human {
}
public void sayHello(Human guy) {
System.out.println("Hello, guy!");
}
public void sayHello(Man guy) {
System.out.println("Hello, man!");
}
public void sayHello(Woman guy) {
System.out.println("Hello, woman!");
}
public static void main(String[] args) {
Human man = new Man();
Human woman = new Woman();
//Man man = new Man();
//Woman woman = new Woman();
Main main = new Main();
main.sayHello(man);
main.sayHello(woman);
}
}
如果使用
Human man = new Man(); Human woman = new Woman();
这两行,输出结果就会是
Hello,guy! Hello,guy!
如果是Man man = new Man(); Woman woman = new Woman();
输出就是Hello, man! Hello, woman!
静态类型就是变量前的类型,实际类型是new 后面的类型,也就是对Human man = new Man()来说,man的静态类型是Human,实际类型是Man,所以根据参数的静态类型,选择对应的方法输出结果。
动态分派主要针对方法的重写,也就是子类重写父类的方法。
Java语言属于静态多分派、动态单分派类型。编译期间,也就是静态分派过程,依据静态类型和参数类型选择,判断条件多于一个,属于多分派;运行期间,就是动态分派过程,由于静态分派时已经确定了方法的参数,这里只需要确定变量的实际类型,来找到对应的重写方法,也就是只需要一个判断条件,所以属于单分派。