Java虚拟机方法调用

21 篇文章 0 订阅
5 篇文章 0 订阅

概述

方法的调用并不等同于方法中的代码被执行,方法调用阶段唯一的任务就是确定被调用方法的版本(即调用哪一个方法),暂时还未涉及方法内部的具体运行过程。Class文件编译的过程不包含传统程序语言的连接步骤,一切方法调用在Class文件里面存储的都只是符号引用,而不是方法在实际运行时内存布局中的地址(也就是之前所说的直接引用)。

解析

在Java语言中“编译期可知,运行期不可变”的方法,会在加载解析阶段会将Class文件常量池中的符号引用转化为直接引用。满足这个要求的方法主要有静态方法和私有方法,前者与类直接相关,后者外部不可被访问,这两种方法的特点决定他们不可能通过继承或别的方式重写出其它版本,因此他们都适合在类加载阶段进行解析。
Java虚拟机支持以下5条方法调字节码指令:

  • invokestatic。调用静态方法。
  • invokespecial。用于调用实例构造器()方法、私有方法和父类中的方法。
  • invokevirtual。用于调用所有虚方法。
  • invokeinterface。用于调用接口方法,会在运行时在确定一个实现改接口的对象。
  • invokedynamic。先在运行时动态解析出调用点限定符所引用的方法,然后再执行该方法。前面4条指令,分派逻辑都固化在Java虚拟机内部,而invokeddynamic指令的分派逻辑是由用户设定引导方法来决定的。

只要能被invokestatic和invokespecial指令调用的方法,都可以在解析阶段确定唯一的调用版本,Java语言符合这个条件的方法共有静态方法、私有方法、实例构造器、父类方法4中,再加上被final修饰的方法(尽管它用invokevirtual指令调用),这5中方法调用会在类加载的时候就可以把符号引用解析为直接引用。

分派

静态分派

定义变量的类型称为“静态类型”(Static Type),或者叫“外观类型”(Apparent Type),用来实例化的类型称为“实际类型”(Actual Type)或者叫“运行时类型”(Runtime Type)。静态类型和实际类型在程序中可能发生变化,区别是静态类型的变化仅仅在使用时发生,变量本身的静态类型不会改变,并且最终的静态类型是在编译期可知的;而实际类型的变化结果在运行期间再能确定,编译期在编译程序的时候并不知道一个对象的实际类型。
编译器在重载方法的时候,是通过参数的静态类型而不是实际类型作为判定依据的。所有依赖静态类型来决定方法执行版本的分派动作,都称为静态分派。静态方法最典型的应用表现就是方法的重载,静态分派发生在方法编译阶段,静态分派是在编译阶段完成的。

动态分派

invokevirtual指令的运行时解析过程大致分为以下几步:

  • 找到操作数栈顶的第一个元素所指向的对象的实际类型。
  • 如果在实际类型中找到与常量中的描述符和简单名称都相符的方法,则进行访问权限的校验,如果通过则直接返回这个方法的直接引用,查找过程结束;不通过则返回java.lang.IllegalAccessError异常。
  • 否则,按照继承关系从下向上一次对实际类型的各个父类进行第二步的搜索和验证。
  • 如果始终没有找到合适的方法,则抛出java.lang.AbstractMethodError异常。

invokevirtual指令不仅会把常量池中的方法符号引用解析到直接引用,还会根据方法的接受者的实际类型来选择方法版本,这个过程就是方法重写的本质。我们把这种在运行期间根据实际类型确定方法执行版本的分派过程称为动态分派。
多态的根源在于虚方法调用指令invokevirtual的执行逻辑,所以多态只对方法有效,对字段无效。当子类声明与父类同名字段时,子类字段会遮蔽父类的同名字段。

单分派和多分派

方法的接收者和方法的参数统称为方法的宗量,根据分派基于多少宗量,可以划分为单分派和多分派两种。单分派是根据一个宗量对目标方法进行选择,多分派是根据多于一个宗量对目标方法进行选择。
根据上面的分析可以得出,Java是一门静态多分派、动态单分派的语言。

虚拟机动态分派的实现

Java虚拟机基于性能的考虑,真正运行的时一般不会频繁地反复搜索类型元数据。面对这种情况,一种基础且常见的优化手段是为类型在方法区中建立一个虚方法表(Virtual Method Table,也成为了vtable,与此对应的,在invokeinterface执行时也会用到接口方法表-Interface Method Table,简称itable),使用虚方法表索引来代替元数据查找以提高性能。
虚方法表存放着各个方法实际入口的地址。如果某个方法在子类中没有被重写,那么子类虚方法表的地址入口和父类相同方法的地址入口是一致的,都是指向父类的实现入口。如果子类中重写了这个方法,子类虚方法表中的地址会被替换为指向子类实现版本的入口地址。
为了程序实现方便,具有相同签名的方法,在父类、子类的虚方法表中都应当具有一样的索引序号,这样当类型变换时,仅需要变更查抄的虚方法表,就可从不同的虚方法表中安索引转换出所需的入口地址。虚方法表一般在类加载连接阶段进行初始化,准备了类的变量初始值后,虚拟机会把该类的虚方法表也一同初始化完毕。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值