JVM是如何执行方法调用的?(下)

1.虚方法的调用

Java里所有非私有实例方法调用都会被编译成invokevirtual指令,而接口方法调用都会被编译成invokeinterface指令。这两种指令,均属于Java虚拟机中的虚方法调用。

在绝大多数情况下,Java虚拟机需要根据调用者的动态类型,来确定虚方法调用的目标方法。这个过程我们称之为动态绑定。那么,相对于静态绑定的非虚方法调用来说,虚方法调用更加耗时。

在Java虚拟机中,静态绑定包括用于调用静态方法的invokestatic指令,和用于调用构造器、私有实例方法以及超类非私有实例方法的invokespecial指令。如果虚方法调用指向一个标记为final的方法,那么Java虚拟机也可以静态绑定该虚方法调用的目标方法。

Java虚拟机中采取了一种空间换取时间的策略来实现动态绑定。它为每个类生成一张方法表,用以快速定位目标方法。

2.方法表

类加载的准备阶段,它除了为静态字段分配内存之外,还会构造与该类相关联的方法表。

这个数据结构,便是Java虚拟机实现动态绑定的关键所在。以invokevirtual所使用的虚方法表(virtual method table,vtable)为例介绍方法表的用法。invokeinterface所使用的接口方法表(interface method table,itable)稍微复杂些,但是原理其实是类似的。

方法表本质上是一个数组,每个数组元素指向一个当前类及其祖先类中非私有的实例方法。

这些方法可能是具体的、可执行的方法,也可能是没有相应字节码的抽象方法。方法表满足两个特质:其一,子类方法表中包含父类方法表中的所有方法;其二,子类方法在方法表中的索引值,与它所重写的父类方法的索引值相同。

方法调用指令中的符号引用会在执行之前解析成实际引用。对于静态绑定的方法调用而言,实际引用将指向具体的目标方法。对于动态绑定的方法调用而言,实际引用则是方法表的索引值(实际上并不仅是索引值)

在执行过程中,Java虚拟机将获取调用者的实际类型,并在该实际类型的虚方法表中,根据索引值获得目标方法。这个过程便是动态绑定。

abstract class Passenger {
  abstract void passThroughImmigration();
  @Override
  public String toString() { ... }
}
class ForeignerPassenger extends Passenger {
	 @Override
 	void passThroughImmigration() { /* 进外国人通道 */ }
}
class ChinesePassenger extends Passenger {
  @Override
  void passThroughImmigration() { /* 进中国人通道 */ }
  void visitDutyFreeShops() { /* 逛免税店 */ }
}

Passenger passenger = ...
passenger.passThroughImmigration();

 

3.内联缓存

内联缓存是一种加快动态绑定的优化技术。它能够缓存虚方法调用中调用者的动态类型,以及该类型所对应的目标方法。在之后的执行过程中,如果碰到已缓存的类型,内联缓存便会直接调用该类型所对应的目标方法。如果没有碰到已缓存的类型,内联缓存则会退化至使用基于方法表的动态绑定。

 

1.单态(monomorphic)指的是仅有一种状态的情况。

2.多态(polymorphic)指的是有限数量种状态的情况。二态(bimonrphic)是多态的其中一种。

3.超多态(megamorphic)指的是更多种状态的情况。通常我们用一个具体数值来区分多态和超多态。在这个数值之下,我们称之为多态。否则,我们称之为超多态。

 

单态内联缓存,便是只缓存了一种动态类型以及它所对应的方法。它的实现非常简单:比较所缓存的动态类型,如果命中,则直接调用对应的目标方法。

 

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

在实践中,大部分的虚方法调用均是单态的,也就是只有一种动态类型。为了节省内存空间,Java虚拟机只采用单态内联缓存。

前面提到,当内联缓存没有命中的情况下,Java虚拟机需要重新使用方法表进行动态绑定。对于内联缓存中的内容,我们有两种选择:

一是替换单态内联缓存中的记录。这种做法就好比CPU中的数据缓存,它对数据的局部性有要求,即在替换内联缓存之后的一段时间内,方法调用的调用者的动态类型应当保持一致,从而能够有效地利用内联缓存。因此,在最坏情况下,我们用两种不同类型的调用者,轮流执行该方法调用,那么每次进行方法调用都将替换内联缓存。也就是说,只有写缓存的额外开销,而没有用缓存的性能提升。

另外一种选择是劣化为超多态状态。这也是Java虚拟机的具体实现方法。处于这种状态下的内联缓存,实际上放弃了优化的机会。它将直接访问方法表,来动态绑定目标方法。与替换内联缓存记录的做法相比,它牺牲了优化的机会,但是节省了写缓存的额外开销。

 

总结与实践

虚方法调用包括invokevirtual指令和invokeinterface指令。如果这两种指令所声明的目标方法被标记为final,那么Java虚拟机会采用静态绑定。

否则,Java虚拟机将采用动态绑定,在运行过程中根据调用者的动态类型,来决定具体的目标方法。

Java虚拟机的动态绑定是通过方法表这一数据结构来实现的。方法表中每一个重写方法的索引值,与父类方法中被重写的方法索引值一致。

在解析虚方法调用时,Java虚拟机会记录下所声明的目标方法的索引值,并且在运行过程中根据这个索引值查找具体的目标方法。

Java虚拟机中的即时编译器会使用内敛缓存来加速动态绑定。Java虚拟机所采用的单态内联缓存将记录调用者的动态类型,以及它所对应的目标方法。

当碰到新的调用者时,如果其动态类型与缓存中的类型匹配,则直接调用缓存的目标方法。

否则,Java虚拟机将该内联缓存劣化为超多态内联缓存,在今后的执行过程中直接使用方法进行动态绑定。

// Run with: java -XX:CompileCommand='dontinline,*.passThroughImmigration' Passenger
public abstract class Passenger {
	 abstract void passThroughImmigration();
  public static void main(String[] args) {
  	Passenger a = new ChinesePassenger();
	Passenger b = new ForeignerPassenger();
    long current = System.currentTimeMillis();
    for (int i = 1; i <= 2_000_000_000; i++) {
      if (i % 100_000_000 == 0) {
        long temp = System.currentTimeMillis();
        System.out.println(temp - current);
        current = temp;
      }
      Passenger c = (i < 1_000_000_000) ? a : b;
      c.passThroughImmigration();
	}
  }
}
class ChinesePassenger extends Passenger {
  @Override void passThroughImmigration() {} 
}
class ForeignerPassenger extends Passenger {
  @Override void passThroughImmigration() {}
}

https://blog.csdn.net/ning0323/article/details/75451955 Jvm 参数设置 CompileCommand

https://www.cnblogs.com/marcotan/p/4256885.html jvm参数设置大全

 

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值