java虚方法调用_Java虚拟机如执行方法调用的(二)?

虚方法调用Java里所有非私有实例方法调用都会被编译成invokevirtual指令.

接口方法调用都会被编译成invokeinterface指令.

这两种指令都属于Java虚方法的调用.

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

在Java虚拟机中, 静态绑定包括:调用静态方法的invokestatic指令.

调用构造器、私有实例方法及超类非私有实例方法的invokespecial指令.

如果虚方法调用指向一个被标记为final的方法,那么Java虚拟机也可以静态绑定该虚方法调用的目标方法.

Java虚拟机采用了一种用空间换时间的策略来实现动态绑定.

它为每个类生成一张「方法表」,有以快速定位目标方法.

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

这个数据结构, 是Java虚拟机实现动态绑定的关键.

方法表本质上是一个数组,每个数组元素指向一个当前类及其祖先类中非私有的实例方法.这些方法可能是具体的、可执行的方法,也可能是没有相应字节码的「抽象方法」.

「方法表」满足两个特性:子类方法表中包含父类方法表中所有的方法.

子类方法在方法表中的索引值, 与它所重写父类方法的索引值相同.方法调用指令中的「符号引用」会在执行之前「解析成实际引用」. 对于静态绑定的方法而言, 「实际引用」将指向「具体的目标方法」. 对于「动态绑定」的方法调用而言, 「实际引用」则是「方法表的索引值」(实际上并不仅仅是索引值).

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

「示例」

// 定义一个抽象的动物类.abstract class Animal{

// 抽象方法, 让子类去实现.public abstract void eat();

@Override

public String toString() {

// TODO Auto-generated method stubreturn super.toString();

}

}

class Dog extends Animal{

@Override

public void eat() {

// TODO Auto-generated method stubSystem.out.println("狗在吃... ");

}

@Override

public String toString() {

// TODO Auto-generated method stubreturn super.toString();

}

}

class Cat extends Animal{

@Override

public void eat() {

// TODO Auto-generated method stubSystem.out.println("猫在吃...");

}

@Override

public String toString() {

// TODO Auto-generated method stubreturn super.toString();

}

public void sleep() {

}

}

public class Test{

public static void main(String args[]){

Animal animal = new Dog();

// Animal animal = new Cat(); animal.eat();

// dosomething }

}

上面简单的示例中, 各个类的「方法表」分别是:各个类的方法表

在示例当中, 「Animal类」的方法表包括两个方法:toString()

eat()

它们分别对应0号和1号.

之所以方法表调换了toString()和eat()方法的位置, 是因为toString()方法的「索引值」需要与「Object」类中同名的方法的「索引值」一致. (为了保持简洁, 暂时不考虑Object类中的其它方法).

「Dog」类的方法表同样也包括两个方法. 其中, 0号方法指向「继承」而来的toString()方法. 1号方法则指向自己重写的「eat」方法.

「Cat」类的方法表中包括3个方法, 除了继承而来的「Animal」类的toString()方法, 自己重写的「eat」方法之外, 还包括独立的「sleep」方法.

测试代码的执行过程可以这么理解,可以把「Java虚拟机」理解与一个饲养员, 每当过来一个动物,饲养员先看看是啥东西(获取动态类型),然后翻出狗/猫对应的饲养手册(获取动态类型的方法表).手册的第1条则显示动物如何吃东西的方法(用1作为索引来查找方法表对应的目标方法).实际上, 使用了「方法表」的「动态绑定」与「静态绑定」相比. 仅仅是多出了几个「内存 解引用操作」.

访问栈上的调用者, 获取调用者的动态类型, 读取该类型的方法表, 读取该方法表中的某个索引值所对应的目标方法.

相对于创建并初始化Java栈帧来说, 这几个解引用操作简直可以忽略不计.动态绑定获取目标方法「虚方法」优化的效果看上去十分美好, 但实际上仅存大于「解释执行」中, 或者「即时编译」代码的最坏情况中.

这是因为「即时编译」还拥有另外两种性能更好的优化手段: 「内联缓存(inlining cache)」和「方法内联(method inlining)」. 先说第一种「内联缓存」.

内联缓存「内联缓存」是一种加快「动态绑定」的优化技术.

它能够「缓存虚方法调用中调用者的动态类型, 以及该类型所对应的目标方法」 .在之后的执行过程中, 如果碰到已「缓存」的类型, 「内联缓存」便会直接调用该类型所对应的「目标方法」. 如果没有碰到已缓存的类型, 「内联缓存」则会退化至使用「基本方法表的动态绑定」.内联缓存查找目标方法

在针对多态的优化手段中, 通常会提及以下三个术语.单态(monomorphic)指的是「仅有一种状态」的情况

多态(polymorphic)批的是「有限数量种状态」的情况.

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

对于「内联缓存」来说, 我们也对应的「单态内联缓存」、「多态内联缓存」、「超多态内联缓存」.

「单态内存缓存」, 就是只「缓存」一种动态类型以及它所「对应的目标方法」.实现: 比较所「缓存的动态类型」, 如果命中, 则「直接调用对应的目标方法」.

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

一般来说, 会将「更加热门的动态类型」放在「前面」. 在实践中, 大部分的「虚方法」调用均是「单态」的, 也就是只有一种「动态类型」.为了节省内存空间,Java虚拟机只采用「单态内联缓存」.

前面所说的, 当「内联缓存」没有命中的情况下, Java虚拟机需要「重新使用方法表进行动态绑定」.对于「内联缓存」中的内容, 我们有两种选择.「替换单态内联缓存中的记录」这种做法, 对数据的局部性有要求, 即在「替换内联缓存」之后的一段时间内, 方法调用的调用者的动态类型应该保持一致, 从而能够有效的复用「内联缓存」.

在最坏的情况下, 用两种不同类型的「调用者」, 轮流执行该方法调用, 那么每次进行方法调用都将替换「内联缓存」. 也就是说, 只有写缓存的额外开销, 而没有用缓存的性能提升.替换单态内存内联缓存中的记录

2. 「劣化为超多态状态」这也是Java虚拟机的具体实现方式.

处于这种状态下的「内联缓存」,实际上放弃了优化的机会.

它将直接访问「方法表」, 来「动态绑定目标方法」.与「替换内联缓存记录」的做法相比, 它牺牲了优化的机会, 但是「节省了写缓存的额外开销」.

虽然「内联缓存」随带「内联」二字, 但是它并没有「内联目标方法」.这里需要明确的是, 任何方法调用除非被内联, 否则都会固定开销.

这些开销来源于「保存程序在该方法中的执行位置、新建、压入和弹出新方法所使用的栈 帧」.

对于极其简单的方法而言, 比如说getter/setter, 这部分「固定开销」占据的CPU时间甚至超过了方法本身.

此外, 在「即时编译」中, 「方法内联」不仅仅能够消除方法「调用的固定开销」, 而且还增加了进一步优化的可能性.

本篇完...

谢谢观看 ~~

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值