深入jvm 04. 方法调用

1、重载与重写

重载:在 java 语言中,同一个类中定义的多个方法,方法名相同,且参数类型不同,称之为重载。此外,如果子类中定义了与父类非private方法同名的方法,且参数类型不同,这同样构成了重载。

重写:当子类定义了与父类非 private 方法同名的方法,且参数类型也相同,分两种情况:①如果两个方法都是 static 的,则子类方法隐藏父类方法;②如果两个方法都不是 static 的,且都不是 private 的,则子类方法重写了父类方法。( java 虚拟机的判定标准略微不同,见第2部分)

重载方法的区分是在编译阶段就完成的,所以可以认为 jvm 不存在重载的概念。而 jvm 中对于重写的判定标准与java语言略微不同。java虚拟机对方法的识别主要在于类名、方法名和方法描述符( method descriptor )。方法描述符是由方法的参数类型和返回值类型组成的。两个方法,如果参数类型和返回值类型都相同,jvm会在类的验证阶段报错。jvm 通过方法描述符来判定方法重写,当子类方法与父类非 private、非 static 方法同名时,如果这两个方法的参数类型与返回值类型都一致,jvm才会认为这是重写。

现在我们知道,java 语言中的重写与 java 虚拟机中的重写,它们的判定标准是不同的,编译器会通过生成桥接方法来实现 java 语言中的重写语义。

class Person{
    
    
	public Number action(double num){
    
    
		...
	}
}

class Man extends Person{
    
    
	@override
	public Double action(double num){
    
    
		...
	}
}

javac 编译器会在 Man 的字节码文件中自动生成一个桥接方法来保证重写语义。反编译后可以看到这个桥接方法的 flags 为 ACC_PUBLIC, ACC_BRIDGE, ACC_SYNTHETIC,其中 ACC_BRIDGE 表示这个一个桥接方法,ACC_SYNTHETIC 表示这个方法由编译器自动生成。

将桥接的字节码翻译成源码:

public Number action(double num){
    
    
	return this.action(num);//调用本类中的action方法
}

如果子类继承父类的一个泛型方法,则编译器会在子类的字节码文件中自动生成桥接方法。

interface Ability {
    
    
	...
}

class Man<T extends Ability> {
    
    
	public double action(double energy, T ability) {
    
    
		...
	}
}

class Superman extends Man<Fly> {
    
    
	@override
	public double action(double energy, Fly ability){
    
    
		...
	}
}

由于泛型擦除,Man 的字节码文件中,T 被擦掉后,实际类型为Ability类型。

public double action(double energy, Ability ability) {
    
    
	...
}

而 Superman 的字节码文件由编译器自动生成了一个桥接方法

public double action(double energy, Ability ability){
    
    
	return this.action(energy, (Fly) ability);
}

2、静态绑定与动态绑定

java 虚拟机中的静态绑定指在解析时就能够识别目标方法的情况,动态绑定指需要在运行时根据调用者的动态类型来识别目标方法的情况。如果 jvm 能够确定目标方法有且只有一个,如被 final 修饰的方法,则可以不通过动态类型绑定而直接确定目标方法。

java字节码中五种方法调用指令:

  1. invokestatic:调用 static 方法。
  2. invokespecial:调用 private 实例方法,构造器,以及使用 super 关键字调用父类的实例方法或构造器,和所实现接口的默认方法。
  3. invokevirtual:用于调用非 private 实例方法。
  4. invokeinterface:用于调用接口方法。
  5. invokedynamic:用于调用动态方法。

对于 invokestatic 和 invokespecial,jvm 可以直接识别目标方法。

对于 invokevirtual 和 invokeinterface,在大部分情况下,jvm 需要在运行时,根据调用者的动态类型,来确定目标方法。

3、符号引用的解析

在字节码文件中,java 编译器会用符号引用指代目标方法。符号引用包括了目标方法所在类/接口的名字、目标方法的方法名和方法描述符。符号引用存储在字节码文件的常量池中。在执行使用了符号引用的字节码之前,jvm 会将这些符号引用解析为实际引用。

对符号引用的解析,可以分成:对非接口符号引用的解析和接口符号引用的解析。

对非接口符号引用的解析,假设该符号引用指向了类C,解析步骤为:
①在 C 中查找符合名字及描述符的方法。
②如果没有找到,在 C 的父类中继续搜索,直至 Object 类。
③如果没有找到,在 C 所直接实现或间接实现的接口中搜索,这一步搜索得到的目标方法必须是非私有、非静态的。并且,如果目标方法在间接实现的接口中,则需满足 C 与该接口之间没有其他符合条件的目标方法。如果有多个符合条件的目标方法,则任意返回其中一个。

对接口符号引用的解析,假设该符号引用指向了接口J,解析步骤为:
①在 J 中查找符合名字及描述符的方法。
②如果没有找到,在 Object 类中的公有实例方法中搜索。
③如果没有找到,则在 J 的超接口中搜索。这一步的搜索结果的要求与非接口符号引用步骤③的要求一致。

符号引用解析为实际引用后,对于静态绑定的方法,实际引用就是目标方法的真实地址。对于动态绑定的方法,实际引用是辅助动态绑定的信息(主要是目标方法在虚方法表的索引值,下面讲述)。

4、方法表

类加载的准备阶段,除了为静态字段分配内存外,还会构造与这个类关联的方法表,用来实现动态绑定。其中,invokevirtual 使用的是虚方法表,invokeinterface 使用的是接口方法表。

方法表实质是一个数组,数组中每个元素指向一个当前类及其祖先类中非私有的实例方法。
图片来源《深入理解java虚拟机》
如果在子类没有重写父类的某个方法,那子类的虚方法表中这个方法的地址将指向父类的实现入口;如果子类重写了父类的某个方法,则子类虚方法表中的这个方法的地址将会替换为子类实现版本的入口地址。如图,Son 重写了 Father 的全部方法,因此 Son 的方法表中并没有指向 Father 的箭头,但是 Son 和 Father 都没有重写Object类的方法,因此它们的方法表中所有从Object继承来的方法都指向了 Object 类。此外,子类方法在方法表中的索引值,与它所重写的父类方法的索引值相同。

使用方法表的动态绑定,首先需要访问栈上的调用者,读取其动态类型,再读取该类型的方法表,然后读取方法表中某个索引值所对应的方法。对动态绑定查方法表的行为,在 jvm 的即时编译器中存在两种优化技术:内联缓存和方法内联。

5、内联缓存技术

内联缓存技术能够缓存虚方法调用中调用者的动态类型,以及该类型的目标方法。在之后的执行过程中,如果碰到已缓存的类型,内联缓存就可以直接调用该类型对应的目标方法。如果没有碰到,内联缓存就会退化为使用基于方法表的动态绑定。

单态内联缓存是指只缓存一种动态类型以及它所对应的目标方法。执行时先比较缓存的动态类型,如果命中,则直接调用对应的目标方法。

多态内联缓存是指缓存了多个动态类型及其目标方法。执行时需要将缓存的多个动态类型,逐个与当前动态类型进行比较,如果命中则调用对应方法。

为了节省空间,jvm 只采用单态内联缓存。

当碰到新的调用者时,如果其动态类型与缓存中的类型一致,则直接调用缓存中的目标方法。否则 jvm 将该内联缓存劣化为超多态内联缓存(超多态的状态比多态还要多),在今后的执行过程中直接使用方法表进行动态绑定。

猜你喜欢

转载自blog.csdn.net/Longstar_L/article/details/107514480