方法调用并不等同于方法执行,方法调用阶段唯一的任务就是确定调用方法的版本(即调用哪一个方法),暂时还不涉及方法内部的具体运行过程。在程序运行时,进行方法调用是最普遍、最频繁的操作。在Class文件的编译过程中不包含传统编译中的连接步骤,一切方法调用在Class文件里存储的都只是符号引用,而不是方法在实际运行时内存布局中的入口地址(相当于直接引用)。这个特性给Java带来了更强大的动态扩展能力,但也使得Java方法的调用过程变得相对复杂,需要在类加载期间甚至到运行期间才能确定目标方法的直接引用。
一、方法解析
所有方法调用中的目标方法在Class文件里面都是一个常量池中的符号引用,在类加载的解析阶段,会将其中一部分符号引用转化为直接引用,这种解析能成立的前提是:方法在程序真正运行之前就有一可确定的调用版本,并且这个方法的调用版本是运行期是不可改变的。换句话说,调用目标在程序代码写好、编译器进行编译时就必须确定下来。这类方法的调用称为解析(Resolution)。
在Java语言中,符合“编译期可知,运行期不可变”这个要求的方法有静态方法和私有方法两大类,前者与类型直接相关联,后者在外部不可被访问,这两种方法都不可能通过继承或者别的方式重写出其它版本,因此它们都适合在类加载阶段进行静态解析。
与之相对应,在Java虚拟机里提供了四条方法调用字节码指令,分别是:
a.invokestatic:调用静态方法
b.invokespecial:调用实例构造器<init>方法,私有方法和父类方法。
c.invokevirtual:调用虚方法。
d.invokeinterface:调用接口方法,会在运行时再确定一个实现此接口的对象。
只要能被invokestatic与invokespecial指令调用的方法,都可以在解析阶段确定唯一的调用版本,符合这个条件的有静态方法,私有方法,实例构造器和父类方法四类,它们在类加载的时候就会把符号引用解析为该方法的直接引用。这些方法可以统称为非虚方法,与之相反,其它方法就称为虚方法(除去final方法)。
Java中的非虚方法除了使用invokestatic与invokespecial指令调用的方法之后还有一种,就是被final修饰的方法。虽然final方法是使用invokevirtual指令来调用的,但是由于它无法被覆盖,没有其它版本,所以也无须对方法接收都进行多态选择,又或者说多态选择的结果是唯一的。在Java语言规范中明确说明了final方法是一种非虚方法。
解析调用一定是个静态过程,在编译期间就完全确定,在类装载的解析阶段就会把涉及的符号引用全部转变为可确定的直接引用,不会延迟到运行期再去完成。而分派(Dispatch)调用则可能是静态的也可能是动态的,根据分派依据的宗量数可分为单分派与多分派。这两类分派方式两两组件就构成了静态单分派,静态多分派,动态单分派与动态多分派情况。
二、分派
1.静态分派
下面是一段程序代码:
- package com.xtayfjpk.jvm.chapter8;
- 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 man) {
- System.out.println("hello man...");
- }
- public void sayHello(Woman woman) {
- System.out.println("hello woman...");
- }
- public static void main(String[] args) {
- Human man = new Man();
- Human woman = new Woman();
- StaticDispatch sd = new StaticDispatch();
- sd.sayHello((Man)man);
- sd.sayHello(woman);
- }
- }
执行结果为:
hello man...
hello guy...
但为什么会选择执行参数为Human的重载呢?在这之前,先按如下代码定义两个重要的概念:Human man = new Man();
上面代码中的“Human”称为变量的静态类型(Static Type)或者外观类型(Apparent Type),后面的“Man”则称为变量的实际类型(Actual Type),静态类型和实际类型在程序中都可以发生一些变化,区别是静态类型的变化仅仅在使用时发生,变量本身的静态类型不会被改变,并且最终的静态类型是编译期可知的;而实际类型变化的结果在运行期才可确定,编译期在编译程序的时候并不知道一个对象的实际类型是什么?如下面的代码:
- //实际类型变化
- Human man = new Man();
- man = new Woman();
- //静态类型变化
- sd.sayHello((Man)man);
- sd.sayHello((Woman)man);
解释了这两个概念,再回到上术代码中。main()里面的两次sayHello()方法调用,在方法接收者已经确定是对象“sr”的前提下,使用哪个重载版本,就完全取决于传入参数和数据类型。代码中刻意定义了两个静态类型相同,实际类型不同的变量,但虚拟机(准确地说是编译器)在重载时是通过参数的静态类型而不是实际类型作为判定依据的。并且静态类型在编译期是可知的,所以在编译阶段,Javac编译器就根据参数的静态类型决定使用哪个重载版本,所以选择了sayHello(Human)作为调用目标,并把这个方法的符号引用写到main()方法的两条invokevirual指令的参数中。
所有依赖静态类型来定位方法执行版本的分派动作,都称为静态分派。静态分派的最典型应用就是方法重载。静态分派发生在编译阶段,因此确定静态分派的动力实际上不是由虚拟机来执行的。另外,编译器虽然能确定出方法的重载版本,但是很多情况下,这个重载版本并不是“唯一的”,往往只能确定一个“更适合的”版本。这种模糊的结论在0和1构成的计算机世界中算是个比较“稀罕”的事件,产生这种模糊结论的主要原因是字面量不需要定义,所以字面量没有显式的静态类型,它的静态类型只能通过语言上的规则去理解和推断。
2.动态分派
动态分派与重写(Override)有着很密切的关联。如下代码:
- package com.xtayfjpk.jvm.chapter8;
- 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();
- }
- }
这里显示不可能是根据静态类型来决定的,因为静态类型都是Human的两个变量man和woman在调用sayHello()方法时执行了不同的行为,并且变量man在两次调用中执行了不同的方法。导致这个现象的原是是这两个变量的实际类型不同。那么Java虚拟机是如何根据实际类型来分派方法执行版本的呢,我们使用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 com/xtayfjpk/jvm/chapter8/DynamicDispatch$Man
- 3: dup
- 4: invokespecial #18 // Method com/xtayfjpk/jvm/chapter8/DynamicDispatch$Man."<init>":()V
- 7: astore_1
- 8: new #19 // class com/xtayfjpk/jvm/chapter8/DynamicDispatch$Woman
- 11: dup
- 12: invokespecial #21 // Method com/xtayfjpk/jvm/chapter8/DynamicDispatch$Woman."<init>":()V
- 15: astore_2
- 16: aload_1
- 17: invokevirtual #22 // Method com/xtayfjpk/jvm/chapter8/DynamicDispatch$Human.sayHello:()V
- 20: aload_2
- 21: invokevirtual #22 // Method com/xtayfjpk/jvm/chapter8/DynamicDispatch$Human.sayHello:()V
- 24: new #19 // class com/xtayfjpk/jvm/chapter8/DynamicDispatch$Woman
- 27: dup
- 28: invokespecial #21 // Method com/xtayfjpk/jvm/chapter8/DynamicDispatch$Woman."<init>":()V
- 31: astore_1
- 32: aload_1
- 33: invokevirtual #22 // Method com/xtayfjpk/jvm/chapter8/DynamicDispatch$Human.sayHello:()V
- 36: return
0-15行的字节码是准备动作,作用是建立man和woman的内存空间,调用Man和Woman类的实例构造器,将这两个实例的引用存放在第1和第2个局部变量表Slot之中,这个动作对应了代码中这两句:
- Human man = new Man();
- Human woman = new Woman();
接下来的第16-21行是关键部分,第16和第20两行分别把刚刚创建的两个对象的引用压到栈顶,这两个对象是将执行的sayHello()方法的所有者,称为接收者(Receiver),第17和第21两行是方法调用指令,单从字节码的角度来看,这两条调用指令无论是指令(都是invokevirtual)还是参数(都是常量池中Human.sayHello()的符号引用)都完全一样,但是这两条指令最终执行的目标方法并不相同,其原因需要从invokevirutal指令的多态查找过程开始说起,invokevirtual指令的运行时解析过程大致分为以下步骤:
a.找到操作数栈顶的第一个元素所指向的对象实际类型,记作C。
b.如果在类型C中找到与常量中描述符和简单名称都相同的方法,则进行访问权限校验,如果通过则返回这个方法的直接引用,查找结束;不通过则返回java.lang.IllegalAccessError错误。
c.否则,按照继承关系从下往上依次对C的各个父类进行第2步的搜索与校验过程。
d.如果始终没有找到合适的方法,则抛出java.lang.AbstractMethodError错误。
由于invokevirtual指令执行的第一步就是在运行期确定接收者的实际类型,所以两次调用中的invokevirtual指令把常量池中的类方法符号引用解析到了不同的直接引用上,这个过程就是Java语言中方法重写的本质。我们把这种在运行期根据实际类型确定方法执行版本的分派过程称为动态分派。
3.单分派与多分派
方法的接收者与方法的参数统称为方法的宗量。根据分派基于多少种宗量,可以将分派划分为单分派与多分派两种。单分派是根据一个宗量来对目标方法进行选择,多分派则是根据多于一个宗量对目标方法进行选择。
在编译期的静态分派过程选择目标方法的依据有两点:一是静态类型;二是方法参数,所以Java语言的静态分派属于多分派类型。在运行阶段虚拟机的动态分派过程只能接收者的实际类型一个宗量作为目标方法选择依据,所以Java语言的动态分派属于单分派类型。所在Java语言是一门静态多分派,动态单分派语言。
4.虚拟机动态分派的实现
由于动态分派是非常频繁的动作,而且动态分派的方法版本选择过程需要在运行时在类的方法元数据中搜索合适的目标方法,因此在虚拟机的实际实现中基于性能的考虑,大部分实现都不会真的进行如此频繁的搜索。面对这种情况,最常用的优化手段就是在类的方法区中建立一个虚方法表(Virtual Method Table,也称vtable,与此对应,在invokeinterface执行时也会用到接口方法表,Interface Method Table,也称itable),使用虚方法表索引来代替元数据据查找以提高性能。
虚方法表中存放着各个方法的实际入口地址。如果某个方法在子类中没有被重写,那么子类的虚方法表里面的地址入口和父类方法的地址入口是一致的,都指向父类的实现入口。如果子类中重写了这个方法,子类方法表中的地址将会被替换为指向子类实现版本的地址入口。
http://blog.csdn.net/xtayfjpk/article/details/41924971?utm_source=tuicool
相关推荐
深入理解 Java 虚拟机笔记 Java 虚拟机(JVM)是 Java 语言的运行环境,它负责解释和执行 Java 字节码。下面是 Java 虚拟机相关的知识点: 虚拟机内存结构 Java 虚拟机的内存结构主要包括以下几个部分: * 方法...
虚拟机栈的生命周期与线程相同,每个方法从调用直至执行完成的过程,就对应着一个栈帧在虚拟机栈中入栈到出栈的过程。 堆区是 JVM 的最大的一块内存区域,是被线程共享的区域,在虚拟机启动时创建。所有类的实例...
除了上述提到的基础知识点外,《深入理解Java虚拟机——JVM高级特性与最佳实践(第2版)》这本书籍还深入探讨了JVM的性能调优、并发编程、以及各种高级特性的具体应用。比如,对于性能调优,书中讲解了如何根据不同的...
### 深入Java虚拟机知识点总结 #### 第一章 Java体系结构介绍 - **Java体系结构概述**:本章主要介绍了Java体系结构的基本概念及其组成部分。Java体系结构旨在为开发者提供一个统一、高效且跨平台的应用开发环境。...
《JVM:深入理解Java虚拟机》是一本深入解析Java虚拟机工作原理和技术细节的经典书籍。这份学习笔记将涵盖JVM的关键概念、架构以及它如何影响Java程序的性能。我们将探讨以下几个方面: 1. **JVM概述** Java虚拟机...
本资源是关于OpenJDK7源码的分析和学习资料,旨在帮助开发者深入理解Java虚拟机的工作原理。 首先,我们来了解一下Java虚拟机的主要组件和功能: 1. **类加载器**:负责加载Java字节码文件(.class),将其转换为...
深入理解Java虚拟机,首先我们要明白Java虚拟机(JVM)的核心功能:它负责装载类文件,执行字节码,并管理内存。Java虚拟机的结构复杂且高效,主要由类装载器、执行引擎、内存管理和类库等组件构成。 类装载器是JVM...
它的设计目标是实现“一次编写,到处运行”,通过Java虚拟机(JVM)确保代码在不同操作系统上都能运行。Java语言的特点包括简洁性、面向对象、健壮性、安全性、高效性和可移植性。 【基本语法】 Java的基本语法包括...
理解这些概念对于深入学习Java和优化Java应用性能至关重要。在面试或日常工作中,掌握JVM的工作原理、JVM与操作系统、JRE和JDK的关系,可以帮助我们更好地理解Java程序的运行机制,解决性能问题,以及进行高效的代码...
《深入理解Java虚拟机》是Java开发者们深入探讨Java运行机制的经典之作,作者周志明以其深入浅出的讲解方式,揭示了Java虚拟机(JVM)的工作原理。本资源包含该书第三版的源码分析及学习笔记,旨在帮助读者更透彻地...
学习Java的第一步是安装Java Development Kit (JDK),它包含了编译、调试和运行Java程序所需的所有工具,如javac编译器和Java虚拟机(JVM)。 3. **基本语法** - **变量与数据类型**:Java有八种基本数据类型,...
### 深入理解Java虚拟机(JVM)的关键知识点 #### 一、Java与Java虚拟机的关系 Java语言的设计者们为了使Java程序能够跨平台运行,引入了一个概念——Java虚拟机(JVM)。简单来说,Java源代码在编译成`.class`...
10. **Java虚拟机(JVM)**:理解JVM的工作原理,包括类加载机制、内存模型(堆、栈、方法区等)和垃圾回收机制,有助于优化程序性能。 11. **Java EE**:如果深入学习,还会涉及到Java企业级应用开发,如Servlet、...
### Java虚拟机(JVM)详解 #### 一、Java虚拟机概述与基本概念 Java虚拟机(JVM)是运行Java字节码的虚拟环境,它位于操作...通过对JVM的深入理解,开发者可以更好地优化程序性能,提高应用程序的稳定性和响应速度。
### 学习笔记之Java虚拟机详解 #### 运行时数据区域概览 ...以上是关于Java虚拟机的一些基础知识和深入理解的内容。通过对这些知识点的学习和掌握,可以帮助开发者更好地理解和优化Java程序的性能。
2. **JRE(Java运行时环境)**:包含Java虚拟机(JVM)、类库和其他支持文件,允许在没有安装JDK的计算机上运行Java应用程序。 #### 二、Java语言特性 Java是一种面向对象的编程语言,具备以下特点: 1. **简单性...
### 深入Java虚拟机JVM类加载学习笔记 #### 一、Classloader机制解析 在Java虚拟机(JVM)中,类加载器(ClassLoader)是负责将类的`.class`文件加载到内存中的重要组件。理解类加载器的工作原理对于深入掌握JVM以及...
Java虚拟机(JVM)是Java程序运行的核心,它是一个抽象的计算机系统,负责执行Java字节码。在深入理解JVM之前,我们先要明白什么是字节码:Java源代码经过编译后生成的中间表示,即.class文件,里面包含的就是字节码...
1. **Java为何高效**:Java的高效主要体现在JVM(Java虚拟机)上,它负责编译字节码并进行垃圾回收,优化内存管理。此外,Java的多线程支持和丰富的类库也是其高效的原因。 2. **IDE(集成开发环境)**:常用的Java...