虚拟机字节码执行引擎
8.1、概述
Java的虚拟机字节码执行引擎是基于栈的字节码解释执行引擎。
8.2、运行时栈帧结构
Java虚拟机,以方法作为最基本的执行单元,栈帧是支持虚拟机进行方法调用和方法执行背后的数据结构。它是虚拟机运行时数据区的Java虚拟机栈的元素。
每一个栈都是线程私有的。
8.2.1、局部变量表
局部变量表,顾名思义,用于存放方法和参数中的局部变量。
局部变量表以变量槽为最小单位存储变量,32位的数据类型占一个变量槽,64位占用两个变量槽。
由于局部变量表是建在Java虚拟机栈中的,所以它是线程私有的。线程安全。
reference 引用类型
- 根据引用,直接或间接的找到对象在Java堆中的数据存放的起始地址或索引。
- 直接或间接通过引用找到对象在方法区内的类型信息。
而c++没有反射的根本原因是:无法获得类型信息。
对于64位的数据类型,Java虚拟机会以高位对齐方式为其分配两个连续的变量槽空间。long double。
Java虚拟机通过索引定位方式使用局部变量表。
当一个方法被调用时,Java虚拟机会使用局部变量表来完成参数值到参数变量列表的转化。也就是实参到形参的传递。
若执行的是实例方法,则局部变量表第一位默认存放的是传递方法所属的对象的实例引用。可以通过this来访问。
后面按照参数列表顺序排列,占用从1开始的局部变量槽。分配完毕后,再根据方法体定义的局部变量的顺序及作用域分配其余的变量槽。
8.2.2、操作数栈
每个线程的Java虚拟机栈的栈帧的局部变量表都有一个操作数栈,也叫做操作栈,这个栈是用来存放包括long,double,在内的任意数据类型。
工作流程:
当一个方法刚刚开始执行的时候,它的操作数栈是空的,在方法执行的过程中,会有各种字节码指令向操作数栈中压入推出内容,也就是入栈出栈操作。
操作数栈的元素类型必须与字节码指令中的序列严格匹配。
操作数栈的数据共用
两个不同栈帧作为不同方法的虚拟机栈的元素,是完全独立的。但是好多虚拟机都会让两个栈帧有一部分重叠,不仅节约了一些空间,并且方法调用时就可以公用一部分数据。
8.2.3、动态连接
每个栈帧都有一个指向运行时常量池中该栈帧所属方法的引用。持有这个引用是为了支持方法调用时的动态链接。
Class文件常量池中存在大量符号引用,
**静态解析:**这些符号引用一部分会在类加载阶段或第一次使用时就被转化为直接引用。
**动态连接:**另一部分就是每一次运行期间都转化为直接引用。
8.2.4、方法返回地址
方法退出的过程:
就是把当前栈帧出栈,①回复上层方法的局部变量表和操作数栈②把返回值压入调用者的操作数栈中③调整PC计数器的值以指向方法调用指令后面的一条指令。
8.3、方法调用
方法调用并不等同于方法的执行。方法调用阶段的唯一任务就是确定被调用方法的版本。
8.3.1、解析
在类加载的解析阶段,将一部分符号引用转化为直接引用。这种解析能够成立的前提是:方法在程序运行之前就有一个可确定的版本。并且运行期间这个版本不可改变。这类方法的调用就被称为解析。
在Java语言中“编译期可知,运行时不可变”方法分为两大类:
①静态方法
②私有方法
Java虚拟机支持的5条方法调用字节码的指令:
- invokestatic 调用静态方法
- invokespecial 调用实例构造器() 方法,私有方法,以及父类的方法。
- invokevirtual 用于调用所有虚方法,以及final修饰的方法。
- invokeinterface 调用接口方法
- invokedynamic 由用户设定引导方法决定。
只要能被invokestatic invokespecial 指令调用的方法,都可在解析阶段唯一确定。
静态方法,私有方法,构造器,父类方法,final方法 统称为**非虚方法**
其余的都成为**虚方法**
8.3.2、分派
1、静态分派
所有依赖静态类型来决定方法执行版本的分派动作,都称为静态分派。
典型代表就是重载。
编译器在重载时,通过参数的静态类型最为判定依据。由于静态类型在编译期可知,所以在编译阶段Javac编译器就确定了调用方法的哪个重载版本。
如下:
class Human{
}
class Man extends Human{
}
calss Woman extends Human{
}
public class StaticDispatch{
public void sayHi(Human){
System.out.println("higuys");
}
public void sayHi(Man){
System.out.println("hiMan");
}
public void sayHi(Woman){
System.out.println("hiWoman");
}
public static void main(String[] args){
Human man = new Man();
Human woman = new Woman();
StaticDispatch sd = new StaticDispatch();
sd.sayHi(man);
sd.sayHi(woman);
}
}
输出:higuys
注意:解析和分派不是二选一的排他关系,它们是在不同层次去筛选,确定目标方法的过程。
比如:静态方法在类加载期间就解析了,但是他也可以拥有重载版本,选择重载版本的过程就是在静态分派完成的。
2、动态分派
动态分派和重写有很密切的联系。
重写,调用方法肯定不是静态的,它会根据实际类型来决定调用的方法。
invokevirtual 运行时解析过程大致分为以下几步
- 找到操作数栈顶的第一个元素所指向的实际类型,记为C
- 如果在类型C中找到和常量中的描述符和简单名称都相同的方法(子类重写了父类方法),返回进行权限校验,通过则返回这个方法的直接引用,查找过程结束。否则返回IllegalAccessError
- 否则,按照继承关系从上往下依次对C的各个父类进行第二部搜索验证过程。
- 若始终未找到合适的方法,抛出AbstractMethodError
正是因为invokevirtual指令的第一步就是在运行期间确定接收者的实际类型,所以,invokevirtual指令并不是简单的把常量池中的符号引用解析为直接引用就结束了,还会根据方法接收者实际类型来选择方法的版本。这就是重写的本质。
既然这种多态性的根源在于虚方法调用指令invokevirtual的执行逻辑,所以,多态只会对方法有效,对字段是无效的。在Java里只有虚方法,永远没有虚字段。当子类声明了和父类相同的字段时,父类的字段被遮蔽了。
如下题:
public class FieldHasNoPolymorphic{
static class Father {
public int money;
public Father() {
money = 2;
showMeMoney();
}
public void ShowMeMoney(){
System.out.println("I am Father,i have $" + money);
}
}
static class Son extends Father {
public int money = 3;
public Son(){
money = 4;
showMeMoney();
}
public void ShowMeMoney(){
System.out.println("I am Son,i have $" + money);
}
}
public static void main(String[] args){
Father gay = new Son();
System.out.println("This gay has $" + gay.money);
}
}
首先,在Son类创建的时候,隐式调用了Father构造函数。Father构造函数中,调用了虚方法ShowMeMoney(),执行的是Son中的showMeMoney() 因为Son此时还未被初始化,故输出“i an son,i have money 0”,最后一句通过静态类型访问到了父类中的money
3、单分派与多分派
方法的接收者与方法的参数统称为方法的宗量。根据分派基于多少种宗量,将分派分为单分派与多分派。
Java语言是一个静态多分派,动态单分派的语言。
4、虚拟机动态分派的实现
为了避免元数据查找,常见的优化方法是在方法区中建立一个虚方法表。
功能
虚方法表中存放着各个方法的实际入口地址。若某个方法在子类中未被重写,那么子类的虚方法表中的地址入口和父类相同。
实现
虚方法表一般在类加载的连接的准备阶段进行初始化,准备了类的变量的初始值后,虚拟机会把该类的虚方法表也一并初始化完毕。