一、方法调用

方法调用不同于方法执行,方法调用阶段的唯一任务就是确定被调用方法的版本(即调用哪一个方法),暂时还不涉及方法内部的具体运行过程。Class文件的编译过程中不包括传统编译器中的连接步骤,一切方法调用在Class文件里面存储的都是符号引用,而不是方法在实际运行时内存布局中的入口地址(直接引用)。也就是需要在类加载阶段,甚至到运行期才能确定目标方法的直接引用。

 二、解析

如前所述,所有的方法调用中的目标方法在Class文件里面都是一个常量池中的符号引用,在类加载阶段,会将其中的一部分符号引用转化为直接引用,这种解析能成立的前提是:方法在程序真正运行之前就有一个可确定的调用版本,并且这个方法的调用版本在运行期间是不可变的。也就是说,调用目标在程序代码写好、编译器进行编译时就必须确定下来,这类方法的调用成为解析。

      JAVA中符号“编译器可知、运行期不可变”的方法包括:静态方法、私有方法两大类。前者与类型直接关联,后者在外部不可被访问,这就决定了他们都不可能通过继承或别的方式重写其版本。因此都适合在类的加载阶段进行解析。

JAVA虚拟机里面提供了5条方法调用字节码指令。分别如下:

invokestatic:调用静态方法

invokespecial:调用实例构造器<init>方法、私有方法和父类方法(super(),super.method())。

invokevirtual:调用所有的虚方法(静态方法、私有方法、实例构造器、父类方法、final方法都是非虚方法)。

invokeinterface:调用接口方法,会在运行时期再确定一个实现此接口的对象。

  invokedynamic:现在运行时期动态解析出调用点限定符所引用的方法,然后再执行该方法,在此之前的4条指令,分派逻辑都是固化在虚拟机里面的,而invokedynamic指令的分派逻辑是由用户所设定的引导方法决定的。

只要能被invokestatic和invokespecial指令调用的方法都可以在解析阶段中确定唯一的调用版本,符合这个条件的有静态方法、私有方法、实例构造器、父类方法4类,它们在类加载阶段就会把符号引用解析为该方法的直接引用。这些方法称为非虚方法(还包括使用final修饰的方法,虽然final方法使用invokevirtual指令调用,因为final方法注定不会被重写,也就是无法被覆盖,也就无需对其进行多态选择)。

解析调用一定是一个静态的过程,在编译期间就可以完全确定,在类装载的解析阶段就会把涉及的符号引用全部转化为可确定的直接引用,不会延迟到运行期去完成。分派调用可能是静态的也可能是动态的,根据分派一句的宗量数可分为单分派和多分派。因此分派可分为:静态单分派、静态多分派、动态单分派、动态多分派。

三、分派

1.静态分派(方法重载):

先看一段代码:

 public class StaticDispatch {
static abstract class Human{ }
static class Man extends Human{ }
static class Woman extends Human{ }
@Test
public void test(){
Human man = new Man();
Human woman = new Woman();
StaticDispatch sr = new StaticDispatch();
sr.sayHello(man);
sr.sayHello(woman); } public void sayHello(Human guy){
System.out.println("Hello guy");
}
public void sayHello(Man guy){
System.out.println("Hello man");
}
public void sayHello(Woman guy){
System.out.println("Hello woman");
}
}

运行结果为:

Hello guy
Hello guy

     要解释上面的现象,先要说明几个概念,看如下代码。

      Human man = new Man();

上面一行代码中,Human成为变量man的静态类型,或者叫做外观类型,后面的Man则称为变量的实际类型,静态类型和实际类型在程序中都可以发生一些变化,区别是静态类型的变化仅仅在使用时发生(比如强制类型转换),变量本身的静态类型不会改变,并且最终的静态类型在编译器就是可知的;实际类型变化的结果在运行期才可以确定,编译器在编译程序的时候并不知道一个对象的实际类型是什么。

     比如如下代码:

//实际类型变化

Human man = new Man();

Human woman = new Woman();

//通过强转实现静态类型变化(变量本身静态类型不变)

sr.sayHello((Man)man);

sr.sayHello((Woman)woman);

   虚拟机(编译器)在确定重载函数版本时是通过参数的静态类型而不是实际类型作为判定依据。因此,在编译阶段,编译器就可以根据静态类型确定使用哪个重载的版本。

2.动态分派(方法重写Override):

为了说明动态分派的概念,先看一段代码:

 public class DynamicDispatch{
static abstract class Human{
protected abstract void sayHello();
}
static class Man extends Human{
@Override
protected void sayHello(){
System.out.println("man say hello");
}
}
static class Woman extends Human{
@Override
protected void sayHello(){
System.out.println("woman say hello");
}
} public static void man(String[] args){
Human man = new Man();
Human woman = new Woman();
man.sayHello();
woman.sayHello();
man = new Woman();
man.sayHello(); } }

输出结果为:

man say hello
woman say hello
woman say hello

熟悉多态的人对上面的结果不会感到惊讶。下面使用javap命令输出这段代码的字节码。

如上所示,方法的调用指令都使用了invokevirtual指令,invokevirtual指令的运行时解析过程大致分为以下几个步骤。

1)找到操作数栈顶的第一个元素(对象引用)所指向的对象的实际类型,记作C;

2)如果在类型C中找到与常量中的描述符和简单名称都相符的方法,则进行访问权限校验,如果通过则返回这个方法的直接引用,查找过程结束;如果不通过,则返回java.lang.IllegalAccessError。

3)否则,按照继承关系从下往上依次对C的各个父类进行第2步的搜索和验证。

4)如果始终没有找到合适的方法,则抛出java.lang.AbstractMethodError异常。

 

     由于invokevirtual指令执行的第一步就是在运行期确定接收者的实际类型,这又是java语言中方法重写产生多态的本质。

3.单分派与多分派

方法的接收者和方法的参数统称为方法的宗量。根据分派基于多少种宗量,可以将分派划分为单分派和多分派。单分派是根据一个宗量对目标方法进行选择,多分派则是根据多于一个宗量对目标方法进行选择。

 import org.junit.Test;

 /**
* Created by chen on 2016/3/23.
*/
public class Dispatch {
static class QQ{ }
static class _360{ } public static class Father{
public void hardChoice(QQ arg){
System.out.println("father choose qq");
}
public void hardChoice(_360 arg){
System.out.println("father choose 360");
}
}
public static class Son extends Father{
public void hardChoice(QQ arg){
System.out.println("son choose qq");
}
public void hardChoice(_360 arg){
System.out.println("son choose 360");
}
}
@Test
public void test(){
Father father = new Father();
Father son = new Son();
father.hardChoice(new _360());
son.hardChoice(new QQ());
}
}

运行结果:

father choose 360
son choose qq

上述有关于hardChoice方法的两次调用,涉及了静态分派和动态分派的过程。

首先看看编译阶段编译器的选择,也就是静态分派的过程(关于重载)。此时选择目标方法的依据有两点:一是静态类型是Father还是Son,而是方法参数是QQ还是_360。此处选择结果最终的产物是产生了两条invokevirtual指令,两条指令的参数分别是指向Father.hardChoice(_360)和Father.hardChoice(QQ)方法的符号引用。因为是根据两个宗量进行分派,所以java语言的静态分派属于多分派类型。

再看看运行阶段虚拟机的选择,也就是动态分派的过程(关于重写),在执行“son.hardChoice(new QQ());”这句代码时,更准确的说,是在执行invokevirtual指令时,由于编译器已经确定了目标方法的签名必须是hardChoice(QQ),虚拟机此时不会关心传过来的参数类型,也就是此时传过来的实际类型、静态类型都不会对产生任何影响。唯一可以对虚拟机的选择产生影响的就是此方法的接收者的实际类型是Father还是Son。因为只有一个宗量作为依据,所以java语言的动态分派属于单分派。

4、虚拟机动态分派的实现

      由于动态分派是非常频繁的动作,而且动态分派的方法版本选择过程需要运行时在类的方法元数据中搜索合适的目标方法,因此在虚拟机的实际实现中基于性能的考虑,大部分实现都不会真正的进行如此频繁的搜索。面对这种情况,最常用的“稳定优化”手段就是为类在方法区中建立一个虚方法表(vtable,熟悉C++的肯定很熟悉。于此对应的,在invokeinterface执行时也会用到接口方法表---itable),使用虚方法表索引来代替元数据查找以提高性能。具体如下图所示:

虚方法表中存放着各个方法的实际入口地址,如果某个方法在子类中没有被重写,那子类的虚方法表里面的地址入口和父类相同方法的入口地址是一致的,都指向父类的实现入口。如果子类重写了这个方法,子类方法表中的地址将会替换为指向子类实现版本的入口地址。如上图所示,Son重写了来自Father的全部方法,因此Son的方法表没有指向Father类型数据的箭头。但是Son和Father都没有重写来自Object的方法,所以他们的方法表中所有从Object继承来的方法都指向了Object的数据类型。

为了程序实现上的方便,具有相同签名的方法,在父类、子类的虚方法表中都应当具有一样的索引号,这样当类型变换时,仅需要变更查找的方法表,就可以从不同的虚方法表中按照索引转换出所需要的方法入口地址。

方法表一般在类加载阶段的连接阶段进行初始化,准备了类变量初始值之后,虚拟机会把该类的方法表也初始化完毕。

图解JVM执行引擎之方法调用的更多相关文章

  1. 第 12 章 JVM执行引擎

    目录 第 12 章 执行引擎 1.执行引擎概述 1.1.执行引擎概述 1.2.执行引擎工作过程 2.Java 代码编译和执行过程 2.1.解释执行和即时编译 2.2.解释器和编译器 3.机器码 指令 ...

  2. JVM(十二):方法调用

    JVM(十二):方法调用 在 JVM(七):JVM内存结构 中,我们说到了方法执行在何种内存结构上执行:Java 方法活动在虚拟机栈中的栈帧上,栈帧的具体结构在内存结构中已经详细讲解过了,下面就让我们 ...

  3. 第47篇-解释执行的Java方法调用native方法小实例

    举个小实例,如下: public class TestJNI { static { // 程序在加载时,自动加载libdiaoyong.so库 System.loadLibrary("dia ...

  4. JVM执行引擎

    1.概述 执行引擎是jvm核心组成部分之一,建立在物理器,硬件和操作系统层面之上,引擎在执行代码时会有解释执行和编译执行两种选择,输入字节码文件,字节码解析输出结果. 2.栈帧 栈帧是用于支持虚拟机进 ...

  5. 深入分析JVM执行引擎

    程序和机器沟通的桥梁 一.闲聊 相信很多朋友在出国旅游,或者与外国友人沟通的过程中,都会遇到语言不通的烦恼.这时候我们就需要掌握对应的外语或者拥有一部翻译机.而笔者只会中文,所以需要借助一部翻译器才能 ...

  6. 多态:JVM是如何进行方法调用的

    在我们平时的工作学习中写java代码时,如果我们在同一个类中定义了两个方法名和参数类型都相同的方法时,编译器会直接报错给我们.还有在代码运行的时候,如果子类定义了一个与父类完全相同的方法的时候,父类的 ...

  7. JVM执行引擎总结(读《深入理解JVM》) 早期编译优化 DCE for java

    execution engine: 运行时栈current stack frame主要保存了 local variable table, operand stack, dynamic linking, ...

  8. JVM执行引擎的执行过程

    摘自深入分析java web技术内幕

  9. JVM(6) 字节码执行引擎

    编译器(javac)将Java源文件(.java文件)编译成Java字节码(.class文件). 类加载器负责加载编译后的字节码,并加载到运行时数据区(Runtime Data Area) 通过类加载 ...

随机推荐

  1. UILabel 自适应宽高

    #import <UIKit/UIKit.h> @interface UILabel (UILabel_LabelHeighAndWidth) + (CGFloat)getHeightBy ...

  2. MVC @Html.TextBoxFor 格式化

    不能使用Html.EditorFor() 因为需要为生成的控件 指定HTML特性 @Html.TextBoxFor(model => model.StartDate, new { Value = ...

  3. 如何升级PowerShell

    背景: 开发的PowerShell 脚本需要使用Invoke-RestMethod命令,发现在老的服务器上不支持这一命令,经过查询得知由于PS版本的问题.涉及到了PS的升级,需要介绍下PowerShe ...

  4. Git命令

    1. 检出项目到本地 git clone git@github.com:michaelliao/gitskills.git 2. 查看当前工作区状态 git status 3. 添加文件或文件夹至版本 ...

  5. 每天成长一点---WEB前端学习入门笔记

    WEB前端学习入门笔记 从今天开始,本人就要学习WEB前端了. 经过老师的建议,说到他每天都会记录下来新的知识点,每天都是在围绕着这些问题来度过,很有必要每天抽出半个小时来写一个知识总结,及时对一天工 ...

  6. [LeetCode] Different Ways to Add Parentheses 添加括号的不同方式

    Given a string of numbers and operators, return all possible results from computing all the differen ...

  7. 常见web攻击以及防御

    xss攻击: 跨站脚本攻击,攻击者在网页中嵌入恶意代码,当用户打开网页,脚本程序便开始在客户端的浏览器上执行,以盗取客户端cookie,用户名密码,下载执行病毒木马程序,甚至是获取客户端admin权限 ...

  8. tkinter事件机制

    一.tkinter.Event tkinter的事件机制跟js是一样的,也是只有一个Event类,这个类包罗万象,集成了键盘事件,鼠标事件,包含各种参数. 不像java swing那种强类型事件,sw ...

  9. Angularjs+node+Mysql实现地图上的多点标注

    注:本文适合对于node有一定基础的人,如果您是小白,请先用1个小时学习node.node文档https://nodejs.org/en/docs/ 该片博文的源码地址:https://github. ...

  10. iOS学习-创建带下划线的button

    UIButton *tempBtn = [UIButton buttonWithType: UIButtonTypeCustom]; tempBtn.frame = CGRectMake(, , , ...