虚拟机栈(下)
栈顶缓存技术
什么是栈顶缓存技术
将栈顶元素在物理CPU的寄存器中,以此降低对内存的读/写次数,提升执行引擎的执行效率。
为什么需要栈顶缓存技术
因为操作数是存储在内存中的,所以频繁的读写会对内存造成很大的负荷进而影响执行速度。
动态链接(指向运行时常量池的方法引用)
每个栈帧都对应着一个方法,且每一个栈帧内部都包含着一个指向运行时常量池该栈对应的方法引用(即动态链接)
动态链接的作用
在Java源文件被编译到字节码文件中时,所有的变量和方法引用都作
为符号引用保存在class文件的常量池里。
一个方法去调用另外的其他方法时,就是通过调用常量池中代表该方法的符号引用来完成的。动态链接的作用就是将这些符号引用转换为调用方法的直接引用。
运行时常量池
上面的动态链接提到了运行时常量池,所以就来讲解一下这个。
运行时常量池位于方法区(注意: JDK1.7 及之后版本的 JVM 已经将运行时常量池从方法区中移了出来,在 Java 堆(Heap)中开辟了一块区域存放运行时常量池)
为什么要运行时常量池?
常量池的作用,就是为了提供一些符号和常量,便于指令的识别。
方法的调用
在JVM中,将符号引用转换为调用方法的直接引用与方法的绑定机制相关。
静态链接
当一个字节码文件被加载进JVM内部时候,如果被调用的方法在编译期间就能确定了并且在运行期不会发生变化的话。这种情况下将调用方法的符号引用转换为直接引用的过程称之为静态链接。
动态链接
如果被调用的方法在编译期间无法被确定下来,只有在程序运行的时候将符号引用转换为直接引用,因为具有动态性所以称为动态链接。
方法的绑定机制
可以分为早期绑定与晚期绑定。
绑定就是一个字段 、或者符号引用等被替换为直接引用的过程,这只会发生一次
早期绑定
早期绑定就是被调用标方法在编译期可知,且运行期保持不变时, 即可将这个方法与所属的类型进行绑定,这样一来,由于明确了被调用的目
标方法究竟是哪一个,因此也就可以使用静态链接的方式将符号引用转换为直接引用
晚期绑定
如果被调用的方法在编译期无法被确定下来,只能够在程序运行期根据实际的类型绑定相关的方法,这种绑定方式也就被称之为晚期绑定。
虚方法与非虚方法
- 如果方法在编译器就确定了具体的调用版本,这个版本在运行时是不可变的。这样的方法称为非虚方法
- 静态方法、私有方法、final方法、实例构造器(实例已经确定,this()表示本类的构造器)、父类方法(super调用)都是非虚方法
其余的都称为虚方法
虚拟机中提供了一下几条方法调用指令
普通调用指令:
invokestatic:调用静态方法,解析阶段确定唯一方法版本;
invokespecial:调用< init >方法、私有及父类方法,解析阶段确定唯一方法版本;
invokevirtual调用所有虚方法;
invokeinterface:调用接口方法;
动态调用指令(Java7新增):
invokedynamic:动态解析出需要调用的方法,然后执行 .
其中 invokestatic指令和 invokespecial指令调用的方法称为非虚方法,其余的(final修饰的除外)称为虚方法。
举例
/**
* 解析调用中非虚方法、虚方法的测试
*/
class Father {
public Father(){
System.out.println("Father默认构造器");
}
public static void showStatic(String s){
System.out.println("Father show static"+s);
}
public final void showFinal(){
System.out.println("Father show final");
}
public void showCommon(){
System.out.println("Father show common");
}
}
public class Son extends Father{
public Son(){
super();
}
public Son(int age){
this();
}
public static void main(String[] args) {
Son son = new Son();
son.show();
}
//不是重写的父类方法,因为静态方法不能被重写
public static void showStatic(String s){
System.out.println("Son show static"+s);
}
private void showPrivate(String s){
System.out.println("Son show private"+s);
}
public void show(){
//invokestatic
showStatic(" 大头儿子");
//invokestatic
super.showStatic(" 大头儿子");
//invokespecial
showPrivate(" hello!");
//invokespecial
super.showCommon();
//invokevirtual 因为此方法声明有final 不能被子类重写,所以也认为该方法是非虚方法
showFinal();
//虚方法如下
//invokevirtual
showCommon();//没有显式加super,被认为是虚方法,因为子类可能重写showCommon
info();
MethodInterface in = null;
//invokeinterface 不确定接口实现类是哪一个 需要重写
in.methodA();
}
public void info(){
}
}
注意:子类中showStatic不是重写的父类的静态方法,因为静态方法不能被重写。
关于invokedynamic指令
之所以出现该指令是JAVA为了实现【动态类型语言】而做的一种改进。
但在Java7中并没有提供直接生成 invokedynamic指令的方法,需要借助ASM这种底层字节码工具来产生 invokedynamic指令。直到Java8的 Lambda表达式的出现, 在Java中才有了直接的生成invokedynamic指令的方式。
这里要解释一下什么是动态类型语言 和静态类型语言。
简而言之:
静态类型语言就是判断变量自身的类型。例如java的int size=3;
动态类型语言判断变量值的类型,变量是没有类型的;例如js中的
var size=“你好”
虚方法表
为什么要虚方法表
因为在面向对象编程的过程中会经常用到动态分派,每次用动态分派都会重新在类的方法区搜索合适的数据,很浪费性能。为了提高性能就采用了虚方法表
分派:是指在Java中对方法的调用。Java中有三大特性:封装、继承和多态。分派是多态性的体现,Java虚拟机底层提供了我们开发中“重写”和“重载”的底层实现。其中重载属于静态分派,而重写则是动态分派的过程。
什么是虚方法表
是JVM在类的方法区建立的一个表(非虚方法不会出现在该表中),使用了索引表来代替查找。而且每一个类中都有一个虚方法表,表中存放着各个方法的实际入口。
虚方法表的创建时间
在类加载的链接阶段被创建并开始初始化,类的变量的初始值准备完毕后JVM也会把该类的虚方法表初始化完毕。
举例1:
Son类继承了Father类,并且重写了里面的两个方法,所以Son类以后要用这两个方法就直接找Son类,而其余的一直没有被修改,还是指向OBJ类,所以直接去找OBJ类。
举例2:
interface Friendly{
void sayHello();
void sayGoodbye();
}
其余三个是类
CockerSpaniel(可卡犬)虚方法表:
cat虚方法表:
dog虚方法表:
方法返回地址
存放调该方法的pc寄存器的值。
PC寄存器存储的是下一条字节码的指令地址,也就是即将要执行的指令,由执行引擎进行读取
当一个方法开始执行后,只有两种方式可以退出这个方法
① 正常结束
②出现未处理的异常,非正常退出
无论通过哪种方式退出,在方法退出后都会返回到该方法被调用的位置。
方法正常退出:调用者的pc计数器的值作为返回地址,即调用该方法的指令的下一条指令的地址。
异常退出:返回地址是要通过异常表来确定,栈帧中一般不会保存这部分信息。
- 正常完成出口和异常完成出口的区别在于:通过异常完成出口退出的不会给他的上层调用者产生任何的返回值。
栈帧中的一些附加信息
栈帧中还允许携带与Java虚拟机实现相关的一些附加信息。例如,对程序调试提供支持的信息。
这些信息不是必须的,是可选的。