栈帧用于帮助虚拟机执行方法调用与方法执行的数据结构。封装了方法的局部变量表、动态链接信息、方法的返回地址以及操作数栈等信息
符号引用、直接引用
局部变量表中存放的基本单位是slot,对于32位及以下的数据一般是占用一个slot,而对于64位的一般是占用两个slot,如long何double;slot是可以复用的,局部变量是有作用域的。
符号引用:有些符号引用会在类加载或在第一次使用的时候就转换成直接引用,这种转换称为静态解析;有一些符号引用会在每次运行期转换成直接引用,这种转换叫做动态链接,着体现为Java的多态性。
静态解析的4种情形:
静态方法
父类方法
构造方法
私有方法(私有方法无法被重写,不支持多态)
以上4类方法称为非虚方法,它们是在类加载阶段就可以将符号引用转换成直接引用的。
方法的静态分派:
示例:
class GrandPa{}
class Father extends GrandPa{}
class Son extends Father{}
public class MyTest4 {
public void test(GrandPa grandpa) {
System.out.println("grandpa");
}
public void test(Father father) {
System.out.println("father");
}
public void test(Son son) {
System.out.println("son");
}
public static void main(String[] args) {
MyTest4 myTest4 = new MyTest4();
GrandPa pa1 = new Father();
GrandPa pa2 = new Son();
myTest4.test(pa1);
myTest4.test(pa2);
}
}
上述示例输出两个grandpa;以代码GrandPa pa1 = new Father();为例,pa1的静态类型是GrandPa,而pa1的实际类型(真正指向的类型)是Father。可以得出结论,变量的静态类型是不会发生变化的,而变量的实际类型则是可以发生变化的(多态的一种体现),实际类型是在运行期才能确定。
上述示例是方法重载,方法重载是一种静态行为,在编译期就可以完全确定;方法的参数取决于静态类型,而不是动态类型。
方法动态分派:
方法的动态分派涉及到一个重要的概念:方法接收者
invokevirtual字节码指令的多态查找流程:
首先去操作数栈顶寻找到栈顶操作数所指向的对象的实际类型,不是静态类型
在常量池中寻找实际类型中是否存在名字以及修饰符与父类方法完全一致的方法,如果找到,并且权限校验也通过,如是否public等,那么就直接执行方法;如果没有找到,就按照继承的关系按照子类向父类继续寻找是否与父类相同的方法,找到了则执行,最终没有找到则返回错误
比较方法重载(overload) 和方法重写(overwrite),可以得到相关结论:方法重载是静态的,是编译器行为,方法重写是动态的,是运行期行为。
重载和重写还有一个重要区别是方法接收者(方法调用者)不同。
示例:
class Fruit{
public void fruitInfo() {
System.out.println("fruit");
}
}
class Oringe extends Fruit{
public void fruitInfo() {
System.out.println("Oringe");
}
}
class Apple extends Fruit{
public void fruitInfo() {
System.out.println("Apple");
}
}
public class MyTest5 {
public static void main(String[] args) {
Fruit oringe = new Oringe();
Fruit apple = new Apple();
oringe.fruitInfo();
apple.fruitInfo();
}
}
上述代码字节码中main方法的指令如下,可以看到字节码中显示的调用类型都是父类中的方法但是invokevirtual会按照多态查找流程去查找执行方法,首先去实际类型中查找是否有与父类完全相同的方法,如果有则直接执行该类型的方法,如果实际类型中没有该方法,则往其父类查找是否存在该方法,找到了则直接执行,最终没有找到则返回错误
针对于方法调用动态分派的过程,虚拟机会在类的方法区建立一个虚方法表的数据结构(virtual method table,vtable),针对于invokeinterface指令来说,虚拟机会建立一个叫做接口方法表的数据结构。
虚方法表中是存放方法的入口地址,是为了优化查找效率。如果子类中没有重写父类中的方法,那么子类的虚方法表中指向该方法的入口地址实际上是指向父类的该方法的入口地址。子类中如果重写了该方法,那么子类中该方法表中方法在虚方法表中的位置与父类中方法在虚方法表中的位置是相同的,比如子类和父类都有test()方法,如果子类中test方法在虚方法表中的位置是第5个,那么在父类中test方法在虚方法表中的位置也会在第5个。
现代jvm在执行Java代码的时候,通常都会将解释执行和编译执行二者结合起来进行。
- 解释执行:通过解释器来读取字节码,遇到相应的指令就去执行该指令
- 编译执行:通过即时编译器(Just In Time,JIT),将字节码转换成本地机器码执行。
- 现代JVM会根据代码热点来生成相应的额本地机器码
基于栈的指令集与基于寄存器的指令集之间的关系:
- jvm执行指令时所采取的方式是基于栈的指令集。
- 基于站的指令集主要的操作有入栈和出栈两种。
- 基于栈的指令集的有事在于它可以在不同平台之间移植,而基于寄存器的指令集是与硬件架构紧密相连的,无法做到可移植性。
- 基于栈的指令集的缺点在于完成相同的操作,指令数量通常要比基于寄存器的指令集数量要多;基于栈的指令集是在内存中完成的,而基于寄存器的之经济是直接由CPU来执行的,它是在告诉缓冲区总进行执行的,速度要快很多,虽然jvm可以采用一些优化手段,但总体来说,基于栈的指令集的执行速度要慢一些。