java 调用弗雷_深入理解java虚拟机(十一) 方法调用-解析调用与分派调用
[方法调用并不等同于方法执行,方法调用阶段唯一的任务就是确定被调用方法的版本(即调用哪一个方法),暂时还不涉及方法内部的具体运行过程。在程序运行时,进行方法调用是最普
方法调用过程是指确定被调用方法的版本(即调用哪一个方法),并不包括方法执行过程。我们知道,Class 文件的编译过程中并不包括传统编译中的连接步骤,一切方法调用在 Class 文件调用里面存储的都只是符号引用,而不是方法在实际运行时的内存布局入口地址,也就是说符号引用解析成直接引用的过程。这个特性使得Java 具有强大的动态扩展能力,但也使得 Java方法调用过程变得复杂起来,需要在类加载器件,甚至是运行期间才确定目标方法的直接饮用。
1、解析调用
在类加载的解析阶段,会将其中一部分符号引用直接转化为直接饮用,前提是:方法在程序真正运行之前就有一个可确定的版本,并且这个方法的调用版本在运行期是不可改变的。换句话说,调用目标在程序代码写好,编译器进行编译时就必须确定下来。这类方法的调用称为解析(Resolution)。
符合这个要求的方法,主要包括:静态方法 和 私有方法。因为这两个方法的特点就决定了他们都不可能通过继承或别的方式重写其他版本。
与之相对应的,Java 虚拟机里面提供了5条方法调用字节码指令,分别如下:[Class文件的编译过程中不包含传统编译中的连接步骤,一切方法调用在Class文件里面存储的都只是符号引用,而不是方法在实际运行时内存布局中的入口地址。]
invokestatic:调用静态方法
invokespecial:调用方法、私有方法和父类方法
invokevirtual:调用所有的虚方法
invokeinterface:调用接口方法,会在运行时在确定一个实现此接口的对象
invokedynamic:会在运行时动态解析出调用电限定符所引用的方法,然后再执行该方法。 只能被 invokestatic 和 invokespecial 调用的方法,都可以在解析阶段中确定唯一的调用版本,符合这个条件的有
静态方法、私有方法、实例构造器、父类方法4类,这些方法称为
非虚方法,由于 final 修饰的方法不能被覆盖,也属于非虚方法。与之相反,其他的方法称为虚方法。
2、分派调用
作为一门面向对象的程序语言,Java 具备面型对象的3个特征:继承、封装和多态。下面我们将会讲解多态性特征的一些最基本的体现,如“重写”和“重载”在Java 虚拟机中是怎么实现的。
2.1静态分派
依赖于静态类型来定位方法执行版本的分派动作(如重载)称为静态分派。 首先我们先来看一下下面的这段代码:
package org.fenixsoft.polymorphic;
/**
* 方法静态分派演示
* @author zzm
*/
public class StaticDispatch {
static abstract class Human {
}
static class Man extends Human {
}
static class Woman extends Human {
}
public void sayHello(Human guy) {
System.out.println("hello,guy!");
}
public void sayHello(Man guy) {
System.out.println("hello,gentleman!");
}
public void sayHello(Woman guy) {
System.out.println("hello,lady!");
}
public static void main(String[] args) {
Human man = new Man();
Human woman = new Woman();
StaticDispatch sr = new StaticDispatch();
sr.sayHello(man);
sr.sayHello(woman);
}
}运行结果如下: hello,guy!
hello,guy!
为什么会选择参数类型为 Human 的重载呢?首先,我们看下面两个重要概念。 Human man = new Man(); 我们将上面代码中的 “Human” 称为变量”man“的 静态类型(Static Type),或者叫做 外观类型(Apparent Type)。后面的 ”Man“则称为变量的实际类型(Actual Type)。 静态类型和实际类型都可以发生变化,区别是静态类型仅仅在使用时变化,变量本身的静态类型不会改变,并且最终的静态类型是编译可知的。而实际类型变化结果只有在运行期才确定,编译器在编译程序的时候,不知道一个对象的实际类型是什么。 虚拟机在重载时是通过参数的静态类型而不是实际类型作为判定依据的。并且静态类型是编译器可知的,因此,在编译期,Javac 编译器会根据参数的静态类型决定使用哪个重载版本,所以选择了 sayHello(Human),并把这个方法的符号引用写入 main() 方法的两条 invokevirtual 指令的参数中。
2.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 main(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命令输出这段代码的字节码:
public static void main(java.lang.String[]);
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=2, locals=3, args_size=1
0: new #16 // class org/bupt/xiaoye/DynamicDispatch$Man
3: dup
4: invokespecial #18 // Method org/bupt/xiaoye/DynamicDispatch$Man."":()V
7: astore_1
8: new #19 // class org/bupt/xiaoye/DynamicDispatch$Woman
11: dup
12: invokespecial #21 // Method org/bupt/xiaoye/DynamicDispatch$Woman."":()V
15: astore_2
16: aload_1
17: invokevirtual #22 // Method org/bupt/xiaoye/DynamicDispatch$Human.sayHello:()V
20: aload_2
21: invokevirtual #22 // Method org/bupt/xiaoye/DynamicDispatch$Human.sayHello:()V
24: new #19 // class org/bupt/xiaoye/DynamicDispatch$Woman
27: dup
28: invokespecial #21 // Method org/bupt/xiaoye/DynamicDispatch$Woman."":()V
31: astore_1
32: aload_1
33: invokevirtual #22 // Method org/bupt/xiaoye/DynamicDispatch$Human.sayHello:()V
36: return
LineNumberTable:
line 28: 0
line 29: 8
line 30: 16
line 31: 20
line 32: 24
line 33: 32
line 34: 36
LocalVariableTable:
Start Length Slot Name Signature
0 37 0 args [Ljava/lang/String;
8 29 1 man Lorg/bupt/xiaoye/DynamicDispatch$Human;
16 21 2 woman Lorg/bupt/xiaoye/DynamicDispatch$Human;
}接下来的16-21句是关键部分。16、20两句将创建的两个对象的引用压到栈顶,这两个对象是将要执行的 sayHello() 方法的所有者,称为接受者(Receiver);17、21句是方法调用指令,但是这两条指令的最终执行的目标方法并不相同。原因就需要从 invokevirtual 指令的动态查找过程开始说起, invokevirtual 指令的运行时解析过程大致分为以下几个步骤:
找到操作数栈顶的第一个元素所指向的对象的实际类型,记做 C。
如果在类型 C 中找到与常量中的描述符和简单名称都相符的方法,则进行访问权限校验。如果通过,则返回这个方法的直接饮用,查找过程结束:如果不通过,则返回 java.lang.IllegalAccessError 异常。
否则,按照继承关系从下往上依次对C 的各个父类进行第2步的搜索和验证过程。
如果始终没有找到合适的方法,则抛出 java.lang.AbstractMethodError 异常。
2.3 单分派与多分派
方法的接受者与方法的参数统称为方法的宗量。根据分派基于多少宗量,可以将分派划分为
单分派和
多分派两种。 我们来看一段代码
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");
}
}
public static void main(String[] args) {
Father father = new Father();
Father son = new Son();
father.hardChoice(new _360());
son.hardChoice(new QQ());
}
}
单分派是根据一个宗量对目标方法进行选择,多分派则是根据多余一个宗量对目标方法进行选择。 在静态分派的过程中,选择目标方法的依据有两点:1、看对象的静态类型时什么,即使 Father 还是 Son。 2、方法参数的类型和数量是什么是 QQ还是 360 。因为是根据两个宗量进行选择,所以 Java 语言的
静态分派属于多分派类型。 在动态分派的过程中,由于编译器已经决定了目标方法的签名,因此只需要找到方法的接受者就可以了。因为是根据一个宗量进行选择,所以 Java 语言的
动态分派属单分派类型。
2.4 动态分配的实现
由于动态分配是非常频繁的动作,而且动态分配的方法版本选择过程需要运行时在
类的方法元数据中搜索合适的目标方法,因此在虚拟机的实际实现中,基于性能的考虑,大部分实现都不会真正的进行如此频繁的搜索。最常用的手段就是为类在方法去中建立一个
虚方法表(Virtual Method Table , 也称为 vtable ,与此对应的,在 invokeinterface 执行时也会用到
接口方法表-Inteface Method Table , 简称 itable),使用虚方法表索引来代替元数据查找以提高性能。 以上面代码为例,虚方法表结构如图:
虚方法表中存放着各个方法的实际入口地址。如果某个方法在子类中没有被重写,那子类的虚方法表里面的地址入口和弗雷相同方法的地址入口是一致的,都指向父类的实现入口。如果子类中重写了这个方法,子类方法表中的地址将会替换为指向子类实现版本的入口。如上图,Son 重写了来自于 Father 的全部方法,因此 Son 的方发表没有指向 Father 类型数据的箭头。但是 Son 和 Father 都没有重写来自于 Object 的方法,所以他们的方法表中所有从 Object 继承来的方法都指向了 Object 的数据类型。 方法表一般在类加载的
连接阶段进行初始化,准备了类的变量初始值之后,虚拟机会把该类的方法表也初始化完毕。[转载:http://blog.csdn.net/ns_code/article/details/17965867方法解析 Class文件的编译过程中不包含传统编译中的连接步骤,一切方法调用在Class文件里面存储的都只是
java 调用弗雷_深入理解java虚拟机(十一) 方法调用-解析调用与分派调用相关推荐
- java 常量折叠_深入理解Java虚拟机之早期编译器优化
Javac编译器 Javac编译器是一个由Java语言编写的程序 Javac的源码与调试 从Sun Javac的代码来看,编译器大致分为3个过程: 解析与填充符号表的过程 插入式注解处理器的注解处理过 ...
- java 异常机制_深入理解Java异常处理机制
一.引子 try-catch-finally恐怕是大家再熟悉不过的语句了,而且感觉用起来也是很简单,逻辑上似乎也是很容易理解.不过,我亲自体验的"教训"告诉我,这个东西可不是想象中 ...
- java事件处理模型_从零开始理解JAVA事件处理机制(3)
我们连续写了两小节的教师-学生的例子,必然觉得无聊死了,这样的例子我们就是玩上100遍,还是不知道该怎么写真实的代码.那从本节开始,我们开始往真实代码上面去靠拢. 事件最容易理解的例子是鼠标事件:我们 ...
- futuretask java 并发请求_图文并茂理解 Java 多线程
优质文章,及时送达 线程 线程的概念,百度是这样解释的: 线程(英语:Thread)是操作系统能够进行运算调度的最小单位.它被包含在进程之中,是进程中的实际运作单位.一条线程指的是进程中一个单一顺序的 ...
- java对象实例_深入理解Java对象实例生成的例子!(转)
深入理解Java对象实例生成的例子!(转)[@more@]代码如下: class A { public int Avar; public A() { System.out.println(" ...
- java class教程_深入理解Java Class文件格式(七)
本专栏列前面的一系列博客, 对Class文件中的一部分数据项进行了介绍. 本文将会继续介绍class文件中未讲解的信息. 先回顾一下上面一篇文章. 在上一篇博客中, 我们介绍了: this_class ...
- 如何理解Java自动装箱_如何理解Java中的自动拆箱和自动装箱?
如何理解Java中的自动拆箱和自动装箱? 自动拆箱?自动装箱?什么鬼,听都没听过啊,这...这..知识盲区... 回到家后小伟赶紧查资料,我透,这不就是问基本类型跟封装类型吗,面试官整啥名词呢... ...
- java画板抽象类_深入理解Java抽象类与接口
基于抽象类与接口有太多相似之处且均体现着oop的抽象性,本文从以下几点谈谈对这两者的理解. 1.抽象类 2.接口 3.各自优缺点以及应用举例. 1.抽象类 在了解抽象类之前,先来了解一下抽象方法.抽象 ...
- java 线程 原子性_深入理解Java多线程与并发框架——Java内存模型与原子性、可见性、有序性...
欢迎关注专栏<Java架构筑基>--专注于Java技术的研究与分享!Java架构筑基zhuanlan.zhihu.comJava架构筑基--专注于Java技术的研究与分享! 后续文章将首 ...
最新文章
- springboot 配置双mysql数据库
- MYSQL数据库VALUES_MYSQL入门大全来啦!
- VC 对话框背景颜色 控件颜色
- TS Introduction(介绍)
- python 文件下载服务器异常_python 从远程服务器下载日志文件的程序
- url后面的参数是什么_揭秘亚马逊黑科技之超级URL原理
- IsPostBack
- 目标检测综述(一:历史由来和古典目标检测的出现)
- html怎么画表格边框,WEB 制作1px边框表格的几种方法
- lesson 12 goodbye and good luck 再见,一路顺风-将来时态-early in the morning
- 交集选择器与并集选择器
- 灵魂书籍 | 《记忆力心理学 | 赫尔曼·艾宾浩斯》
- MATLAB 绘制论文图片格式设置万能代码模板
- html 中输入千分号、万分号
- python阴阳鱼绘制(使用turtle)
- 拉结尔微信好友不同服务器,拉结尔大区互通介绍 玩家轻松跨区组队打怪
- 计算机移动存储设备是,移动存储器在计算机操作系统安装中的应用
- sqlmap命令手册
- 华为畅玩5 android6.0,华为荣耀畅玩5的报价以及具体评测【图文】
- 怎么用计算机求logo,pclogo小海龟里帮我设计一个复杂图