JVM是如何执行方法调用的
在java程序中,一个类中出现多个名字相同,并且参数也相同的方法,那么他们是无法编译通过的。
重载:
在同一个类中定义名字相同的方法,但是参数的类型和个数不同,这些方法之间的关系就是重载。重载的方法在编译期就完成识别。具体到每一个方法调用,java编译器会根据所传入的参数的声明类型来选择重载方法。重载方法在编译阶段已经完成。
选择过程分为三个阶段:
1 不考虑对基本类型的自动拆装箱,以及可变长参数的情况下选取重载方法。
2 上述情况下没有找到适配的方法,那么在允许自动拆箱的,不允许可变长参数的情况下选取重载方法。
3 上述情况未找到适配的方法,那么在允许自动拆箱的,允许可变长参数的情况下选取重载方法。
重载也可以发生在父子类中。如果子类定义了与父类中非私有方法同名的方法,而且这两个方法的参数类型不同,那么在子类中,这两个方法构成了重载。
重写:
父子类中如果两个方法都是静态的,那么子类的方法隐藏了父类的方法。如果这两个方法都不是静态的也不是私有的,方法名和参数类型相同,那么子类的方法重写了父类的方法。JAVA一个很重要的特性便是多态,方法重写就是多态的体现。允许子类继承父类的部分功能,也可以拥有自己独特的行为。
JVM的静态绑定和动态绑定:
某些文章中也称重载为静态绑定,重写为动态绑定。静态绑定:JVM在解析时便能够直接识别目标方法。动态绑定:需要在运行过程中根据调用者的动态类型来识别目标方法。
JVM识别虚拟机方法的关键在于类名,方法名以及方法描述符。方法描述符是由方法的参数类型和返回类型构成的。同一个类中同时出现多个名字相同切描述符也相同的方法,那么JVM在验证阶段报错。JVM能够通过方法描述符准确的识别目标方法。
JVM中关于方法的重写的判定就是基于方法描述符的。也就是说,如果子类定义了与父类中非私有的,非静态的同名方法。那么只有当这两个方法的参数类型和返回方法一致,JVM才会判定重写。
JAVA字节码调用相关的指令:
1 invokestatic: 调用静态方法。
2 invokespecial: 调用私有实例方法,构造器,super调用父类的实例方法和构造器,和实现接口的默认方法。
3 invokevirtual: 调用非私有实例方法。
4 invokeinterface: 调用接口方法。
5 invokedynamic: 调用动态方法。
调用指令的符号引用:
编译过程中,我们并不知道目标方法的具体内存地址,JAVA编译器会暂时用符号引用来表示该目标方法。符号引用包括目标方法所在的类或接口的名字,以及目标方法的文件名和方法描述符。
符号引用存储在class文件的常量池中。在执行使用符号引用的字节码前,JVM需要解释这些符号引用,并替换成实际引用。对于可以静态绑定的方法调用而言,实际引用是一个指向方法的指针。对于需要动态绑定的方法调用而言,实际引用是一个方法表的索引。
虚方法:
java里所有非私有实例方法调用都会被编译成invokevirtual指令。而接口方法调用都会被编译成invokeinterface指令。这两种指令都属于JVM的虚方法调用。
静态绑定包括用于调用静态方法的invokestatic指令,和调用构造方法,私有实例方法以及超类非私有实例方法的invokeSpecial指令。
JVM根据调用者的动态类型,来确定虚方法调用的目标方法,这个过程称为动态绑定。JVM用一种空间换时间的策略来实现动态绑定。
方法表:
类加载的准备阶段,除了静态字段的内存分配外,还会构造与该类相关联的方法表。这个数据结构是JVM实现动态绑定的关键所在。
方法表本质是一个数组。每个数组元素指向一个当前类以及祖先类中非私有的实例方法。方法表满足俩个特质:1 子类方法表中包含父类方发表中所有方法。 2 子类方法在方法表中的索引值与所重写的父类方法的索引值相同。对于静态绑定的方法调用而言,实际引用将指向具体的目标方法。对于动态绑定的方法调用而言,实际引用则是方法表的索引值。在执行过程中,JVM将获取调用者的实际类型,并在该实际类型的虚方法表中,根据索引值获得目标方法。
实际上,使用方法表的动态绑定与静态绑定相比,仅仅多出几个内存引用操作,访问栈上的调用者,读取调用者的动态类型,读取该类型的方法表,读取方法表中某个索引值对应的目标方法。
但是即时编译还拥有两种性能更好的优化手段:内联缓存和方法内联。
内联缓存:
内联缓存是一种加快动态绑定的优化技术。他能够缓存虚方法调用中调用者的动态类型,以及该类型所对应的目标方法。之后的执行过程中,如果碰到已缓存的类型,内联缓存变回直接调用该类型所对应的目标方法,如果没有碰到已缓存的类型,内联缓存则退化到使用基于方法表的动态绑定。
这部分资料看的脑壳疼,看了好多遍算是理顺了。。。