执行引擎是 Java 虚拟机最核心的组成部分之一。所有的 Java 虚拟机的执行引擎都是一致的:输入字节码文件,执行字节码解析的等效过程,输出结果。
1. 运行时栈帧结构
栈帧是用于支持虚拟机进行方法调用和方法执行的数据结构,它是虚拟机运行时数据区中的虚拟机栈的栈元素。栈帧存储了方法的局部变量表、操作数栈、动态连接和方法返回地址等信息。编译程序代码时,栈帧需要分配的内存已经确定,不会受到程序运行期变量数据的影响。
- 局部变量表:存放方法参数和方法内部定义的局部变量。以变量槽(Slot)为最小单位。变量槽有索引,第 0 位索引默认是传递方法所属对象实例的引用,在方法中可以通过关键字“this”来访问到这个隐含的参数。局部变量和类变量不同,变量定义了但没有赋初始值是不能使用的。
- 操作数栈:方法刚开始时栈是空的,各种字节码指令往操作栈中写入和提取内容。例如:算数运算通过操作栈实现;调用其它方法时通过通过操作数栈传递参数。
- 动态连接:指向运行时常量池中该栈帧所属方法的引用,为了支持方法调用过程中的动态连接。
- 方法返回地址:方法退出时,需要返回上层调用的位置。由此处保存。
2. 方法调用
方法调用在 Class 文件里面存储的都是符号引用,需要在类加载期间,甚至到运行期间才能确定内存的入口地址(直接引用)
2.1 解析
在解析阶段,会将一部分符号引用转化为直接引用,前提是方法在运行前有一个可确定的调用版本,并且在运行期不可变。这类方法的调用称为解析。符合这个条件的有静态方法、私有方法、实例构造器、父类方法 4 类。
// 方法静态解析
public class StaticResolution {
public static void sayHello() {
System.out.println("hello world");
}
public static void main(String[] args) {
StaticResolution.sayHello();
}
}
2.2 分派
分派调用可能是静态的也可能是动态的,根据分派依据的宗量数可分为单分派和多分派。
2.2.1 静态分派
静态分派发生在编译阶段,典型应用是方法重载。
// 方法静态分派
public class StaticDispatch {
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,gentleman!");
}
public void sayHello(Woman guy) {
System.out.println("hello,lady!");
}
public static void main(String[] args) {
Human man = new Man();
Human woman = new Woman();
StaticDispatch sr = new StaticDispatch();
sr.sayHello(man);
sr.sayHello(woman);
}
}
/**
输出:
hello,guy!
hello,guy!
**/
在Human man = new Man(); 中,“Human”称为变量的静态类型,“Man”则称为变量的实际类型。静态类型在编译期可知,实际类型变化的结果在运行期才能确定。所以,编译器在编译程序的时候并不知道一个对象的实际类型是什么。
所以在编译阶段,Javac 编译器会根据参数的静态类型决定使用哪个重载版本。实际上,调用的目标是sayHello(Human)。
//重载方法匹配优先级
public class Overload {
public static void sayHello(Object arg) {
System.out.println("hello Object");
}
public static void sayHello(int arg) {
System.out.println("hello int");
}
public static void sayHello(long arg) {
System.out.println("hello long");
}
public static void sayHello(Character arg) {
System.out.println("hello Character");
}
public static void sayHello(char arg) {
System.out.println("hello char");
}
public static void sayHello(char... arg) {
System.out.println("hello char ...");
}
public static void sayHello(Serializable arg) {
System.out.println("hello Serializable");
}
public static void main(String[] args) {
sayHello('a');
}
}
匹配的优先级为:char,int,long,Character,Serializable,Object,char …
2.2.2 动态分派
动态分派和重写关联密切。
// 方法动态分派
public class DynamicDispatch {
static abstract class Human {
protected abstract void sayHello();
}
static class Man extends Human {
@Override
protected void sayHello() {
System.out.println("man say hello");
}
}
static class Woman extends Human {
@Override
protected void sayHello() {
System.out.println("woman say hello");
}
}
public static void main(String[] args) {
Human man = new Man();
Human woman = new Woman();
man.sayHello();
woman.sayHello();
man = new Woman();
man.sayHello();
}
}
/**
输出:
man say hello
woman say hello
woman say hello
**/
运行步骤:
- 找到操作数栈顶的第一个元素所指向的对象的实际类型,记住 C。
- 如果在类型 C 中找到与常量中的描述符和简单名称都相符的方法,则进行访问权限校验,如果通过则返回这个方法的直接引用,查找过程结束;如果不通过,则返回 java.lang.IllegalAccessError 异常。
- 否则,按照继承关系从下往上依次对 C 的各个父类进行第 2 步的搜索和验证过程。
- 如果始终没有找到合适的方法,则抛出 java.lang.AbstractMethodError 异常。
这种运行时动态的确定执行版本的过程,称为动态分派。
2.2.3 单分派与多分派
方法的接受者与方法的参数统称为方法的宗量。单分派是根据一个宗量对目标方法进行选择,多分派则是根据多于一个宗量对目标方法进行选择。
public class Dispatch {
static class QQ {}
static class _360 {}
public static class Father {
public void hardChoice(QQ arg) {
System.out.println("father choose qq");
}
public void hardChoice(_360 arg) {
System.out.println("father choose 360");
}
}
public static class Son extends Father {
public void hardChoice(QQ arg) {
System.out.println("son choose qq");
}
public void hardChoice(_360 arg) {
System.out.println("son choose 360");
}
}
public static void main(String[] args) {
Father father = new Father();
Father son = new Son();
father.hardChoice(new _360());
son.hardChoice(new QQ());
}
}
/**
输出:
father choose 360
son choose qq
**/
静态分派需要静态类型和参数确定,所以是多分派的。而动态分派已经确定了参数信息,只需要确定方法接受者的实际类型,所以动态分派是单分派。