8.方法的调用
静态链接与动态链接
静态链接:
当一个字节码文件被装载进JVM内部时,如果被调用的目标方法在编译期可知,且运行期内保持不变时。这种情况下将调用方法的符号引用转换为直接引用的过程成为静态链接。
动态链接:
如果被调用的方法在编译期无法被确定下来,也就是说,只能够在程序运行期将调用方法的符号引用转换为直接引用,由于这种引用转换过程具备动态性,也就称为动态链接
对应的方法绑定机制为:早期绑定和晚期绑定。
绑定是一个字段、方法或者类在符号引用被替换为直接引用的过程,仅发生一次。
早期绑定:编译器可知,运行期不变
晚期绑定:运行期根据实际类型绑定相应的方法
class Animal{
public void eat(){
System.out.println("动物进食");
}
}
interface Huntable{
void hunt();
}
class Dog extends Animal implements Huntable{
@Override
public void hunt() {
System.out.println("捕食耗子,多管闲事");
}
public void eat(){
System.out.println("狗吃骨头");
}
}
class Cat extends Animal implements Huntable{
public Cat(){
super();//早期绑定
}
public Cat(String name){
this();//早期绑定
}
@Override
public void hunt() {
System.out.println("捕食耗子,天经地义");
}
public void eat(){
System.out.println("猫吃鱼");
}
}
public class AnimalTest {
public static void main(String[] args) {
}
public void showAnimal(Animal animal){
animal.eat();//晚期绑定
}
public void showHunt(Huntable h){
h.hunt();//晚期绑定,不确定实现类
}
}
面向过程的语言只存在早期绑定
面向对象的语言具备多态特性,具备早期绑定和晚期绑定。
Java中任何一个普通方法其实都具备虚函数的特征(允许父类的指针指向子类的实例),它们相当于c++语言的虚函数(c++语言中使用virtual来显式定义)。如果不希望方法具有虚函数特征,可以使用关键字final。
非虚方法:在编译器就确定了具体的调用版本,这个版本在运行时是不可变的。
如:静态方法、私有方法、final方法、实例构造器(构造器不能被重写)、父类方法(子类调用父类的某一个确定的方法)。(其它方法称为虚方法。)
子类对象多态性的使用前提:1. 类的继承关系 2.方法的重写
虚拟机中方法调用指令
普通调用指令
- invokestatic :调用静态方法
- invokespecial:掉用方法、私有及父类方法
- invokevirtual:调用所有虚方法
- invokeinterface:调用接口方法
- (动态调用指令)invokedynamic:动态解析出需要调用的方法,然后执行
前4条指令固化在虚拟机内部,方法的调用执行不可认为干预,而invokedynamic则由用户确定方法版本。其中invokestatic 和invokespecial调用的方法称为非虚方法,其余的(除去final修饰的)称为虚方法。
package learn.jvm.simple.invoke;
/*
前4种指令
auther:shk
解析调用中非虚方法和虚方法的测试
*/
class Father{
public Father(){
System.out.println("father的构造器");
}
public static void showStatic(String str){
System.out.println("father" + str);
}
public final void showFinal(){
System.out.println("father show final");
}
public void showCommon(){
System.out.println("father 普通方法");
}
}
public class Son extends Father{
public Son(){
//invokespectial
super();
}
public Son(int age){
//invokespectial
this();
}
//不是重写的静态方法,因为静态方法不能被重写
public static void showStatic(String str){
System.out.println("son" + str);
}
private void showPrivate(String str){
System.out.println("son private" + str);
}
public void show(){
showStatic("ahchdh"); // invokestatic
super.showStatic("good1"); //invokestatic
showPrivate("hello"); //invokespectial
super.showCommon(); //invokespecial
showCommon(); //invokevirtual
showFinal(); //invokevirtual ,因为是final方法,非虚方法
super.showFinal(); //invokespectial
info(); //invokevirtual
MethodInterface in = null;
in.method();//invokeinterface
}
private void info() {
}
public void display(Father f){
f.showCommon();
}
public static void main(String[] args) {
Son son = new Son();
son.show();
}
}
interface MethodInterface{
void method();
}
动态语言与静态语言
java7增加了invokedynamic指令,为了实现动态类型语言 支持做的改进,但是并没有提供invokedynamic指令的生成方法,需要借助ASM这种底层字节码工具来生成。
java8的lambda表达式的出现,invokedynamic指令有了直接生成的方式
动态类型语言和静态类型语言区别是对类型的检查是在编译期还是在运行期,前者动态类型语言,后者静态类型语言。
静态类型语言是判断变量自身的类型信息;动态类型语言是判断变量值的类型信息,变量没有类型信息。
动态类型语言:JS、Python
静态类型语言:Java(整体,除了lambda表达式)
package learn.jvm.simple.invoke;
@FunctionalInterface
interface Func{
public boolean func(String str);
}
public class Lambda {
public void lambda(Func func){
return;
}
public static void main(String[] args) {
Lambda lambda = new Lambda();
Func func = s->{
return true;
}; // invokedynamic
lambda.lambda(func);
lambda.lambda(s->{
return true;
});
}
}
方法重写的本质
- 知道到操作数栈顶的第一个元素所执行的对象的实际类型,记作C
- 如果在类型C中找到与常量的描述符合简单名称都相符的方法,则进行访问权限校验,如果通过则返回这个方法的直接引用,查找过程结束;如果不通过,则返回java.lang.IllegalAccessError异常
- 否则,按照继承关系从下到上依次对C的各个父类进行第2步的搜索和验证过程
- 如果始终没有找到合适的方法,则抛出java.lang.AbstractMethodError异常
IllegalAccessError异常:当程序试图访问一个属性或方法时,没有权限。一般会引起编译器异常。如果发生在运行时,则说明一个类发生了不兼容的改变。
为了提高性能,JVM在类的方法区简历一个虚方法表。使用索引表来代替查找。
每个类中都有一个虚方法表,表中存放着各个方法的实际入口。
虚方法表会在类加载的链接阶段被创建并开始初始化,类的变量初始值准备完成之后,JVM会把该类的方法表也初始化完毕。
类加载过程
验证(Verify) | 准备(Prepare) | 解析(Resolve) |
---|---|---|
+ 目的在于确保Class文件的字节流中包含信息符合当前虚拟机要求,保证被加载类的正确性,不会危害虚拟机自身安全。 + 主要包含四种验证:文件格式验证,元数据验证,字节码验证,符号引用验证。 | + 为类变量分配内存并且设置该类变量的默认初始值,即零值。 + 这里不包括final修饰的static,因为final在编译的时候就会分配了,准备阶段会显式初始化 + 这里不会为实例变量分配初始化,类变量会分配在方法区,而实例变量是会随着对象一起分配到java堆中 | + 将常量池内的符号引用转换为直接引用的过程(方法的调用)。 + 事实上解析操作往往伴随着JVM在执行完初始化之后再执行 + 符号引用就是一组符号来描述所引用的对象,符号引用的字面量形式明确定义在《java虚拟机规范》的Class文件格式中。直接引用就是直接指向目标的指针、相对偏移量或一个间接定位目标的句柄 + 解析动作主要针对类或接口、字段、类方法、接口方法、方法类型;对应常量池中的CONSTANT_Class_info, CONSTANT_Fieldref_info, CONSTANT_Methodref_info等 |