`
m635674608
  • 浏览: 4932101 次
  • 性别: Icon_minigender_1
  • 来自: 南京
社区版块
存档分类
最新评论

深入理解Java虚拟机笔记---方法调用

    博客分类:
  • jvm
 
阅读更多

 方法调用并不等同于方法执行,方法调用阶段唯一的任务就是确定调用方法的版本(即调用哪一个方法),暂时还不涉及方法内部的具体运行过程。在程序运行时,进行方法调用是最普遍、最频繁的操作。在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.静态分派
   下面是一段程序代码:

[java] view plaincopy在CODE上查看代码片派生到我的代码片
 
  1. package com.xtayfjpk.jvm.chapter8;  
  2.   
  3. public class StaticDispatch {  
  4.       
  5.     static abstract class Human {  
  6.           
  7.     }  
  8.     static class Man extends Human {  
  9.           
  10.     }  
  11.     static class Woman extends Human {  
  12.           
  13.     }  
  14.       
  15.     public void sayHello(Human guy) {  
  16.         System.out.println("hello guy...");  
  17.     }  
  18.     public void sayHello(Man man) {  
  19.         System.out.println("hello man...");  
  20.     }  
  21.     public void sayHello(Woman woman) {  
  22.         System.out.println("hello woman...");  
  23.     }  
  24.       
  25.     public static void main(String[] args) {  
  26.         Human man = new Man();  
  27.         Human woman = new Woman();  
  28.         StaticDispatch sd = new StaticDispatch();  
  29.         sd.sayHello((Man)man);  
  30.         sd.sayHello(woman);  
  31.     }  
  32. }  

 

执行结果为:

hello man...
hello guy...

但为什么会选择执行参数为Human的重载呢?在这之前,先按如下代码定义两个重要的概念:Human man = new Man();
   上面代码中的“Human”称为变量的静态类型(Static Type)或者外观类型(Apparent Type),后面的“Man”则称为变量的实际类型(Actual Type),静态类型和实际类型在程序中都可以发生一些变化,区别是静态类型的变化仅仅在使用时发生,变量本身的静态类型不会被改变,并且最终的静态类型是编译期可知的;而实际类型变化的结果在运行期才可确定,编译期在编译程序的时候并不知道一个对象的实际类型是什么?如下面的代码:

[java] view plaincopy在CODE上查看代码片派生到我的代码片
 
  1. //实际类型变化  
  2. Human man = new Man();  
  3. man = new Woman();  
  4.   
  5. //静态类型变化  
  6. sd.sayHello((Man)man);  
  7. sd.sayHello((Woman)man);  


   解释了这两个概念,再回到上术代码中。main()里面的两次sayHello()方法调用,在方法接收者已经确定是对象“sr”的前提下,使用哪个重载版本,就完全取决于传入参数和数据类型。代码中刻意定义了两个静态类型相同,实际类型不同的变量,但虚拟机(准确地说是编译器)在重载时是通过参数的静态类型而不是实际类型作为判定依据的。并且静态类型在编译期是可知的,所以在编译阶段,Javac编译器就根据参数的静态类型决定使用哪个重载版本,所以选择了sayHello(Human)作为调用目标,并把这个方法的符号引用写到main()方法的两条invokevirual指令的参数中。
   所有依赖静态类型来定位方法执行版本的分派动作,都称为静态分派。静态分派的最典型应用就是方法重载。静态分派发生在编译阶段,因此确定静态分派的动力实际上不是由虚拟机来执行的。另外,编译器虽然能确定出方法的重载版本,但是很多情况下,这个重载版本并不是“唯一的”,往往只能确定一个“更适合的”版本。这种模糊的结论在0和1构成的计算机世界中算是个比较“稀罕”的事件,产生这种模糊结论的主要原因是字面量不需要定义,所以字面量没有显式的静态类型,它的静态类型只能通过语言上的规则去理解和推断。

2.动态分派
   动态分派与重写(Override)有着很密切的关联。如下代码:

[java] view plaincopy在CODE上查看代码片派生到我的代码片
 
  1. package com.xtayfjpk.jvm.chapter8;  
  2.   
  3. public class DynamicDispatch {  
  4.     static abstract class Human {  
  5.         protected abstract void sayHello();  
  6.     }  
  7.     static class Man extends Human {  
  8.         @Override  
  9.         protected void sayHello() {  
  10.             System.out.println("man say hello");              
  11.         }  
  12.     }  
  13.     static class Woman extends Human {  
  14.         @Override  
  15.         protected void sayHello() {  
  16.             System.out.println("woman say hello");  
  17.         }  
  18.     }  
  19.       
  20.     public static void main(String[] args) {  
  21.         Human man = new Man();  
  22.         Human woman = new Woman();  
  23.         man.sayHello();  
  24.         woman.sayHello();  
  25.         man = new Woman();  
  26.         man.sayHello();  
  27.     }  
  28. }  



   这里显示不可能是根据静态类型来决定的,因为静态类型都是Human的两个变量man和woman在调用sayHello()方法时执行了不同的行为,并且变量man在两次调用中执行了不同的方法。导致这个现象的原是是这两个变量的实际类型不同。那么Java虚拟机是如何根据实际类型来分派方法执行版本的呢,我们使用javap命令输出这段代码的字节码,结果如下:

[plain] view plaincopy在CODE上查看代码片派生到我的代码片
 
  1. public static void main(java.lang.String[]);  
  2.   flags: ACC_PUBLIC, ACC_STATIC  
  3.   Code:  
  4.     stack=2, locals=3, args_size=1  
  5.        0: new           #16                 // class com/xtayfjpk/jvm/chapter8/DynamicDispatch$Man  
  6.        3: dup  
  7.        4: invokespecial #18                 // Method com/xtayfjpk/jvm/chapter8/DynamicDispatch$Man."<init>":()V  
  8.        7: astore_1  
  9.        8: new           #19                 // class com/xtayfjpk/jvm/chapter8/DynamicDispatch$Woman  
  10.       11: dup  
  11.       12: invokespecial #21                 // Method com/xtayfjpk/jvm/chapter8/DynamicDispatch$Woman."<init>":()V  
  12.       15: astore_2  
  13.       16: aload_1  
  14.       17: invokevirtual #22                 // Method com/xtayfjpk/jvm/chapter8/DynamicDispatch$Human.sayHello:()V  
  15.       20: aload_2  
  16.       21: invokevirtual #22                 // Method com/xtayfjpk/jvm/chapter8/DynamicDispatch$Human.sayHello:()V  
  17.       24: new           #19                 // class com/xtayfjpk/jvm/chapter8/DynamicDispatch$Woman  
  18.       27: dup  
  19.       28: invokespecial #21                 // Method com/xtayfjpk/jvm/chapter8/DynamicDispatch$Woman."<init>":()V  
  20.       31: astore_1  
  21.       32: aload_1  
  22.       33: invokevirtual #22                 // Method com/xtayfjpk/jvm/chapter8/DynamicDispatch$Human.sayHello:()V  
  23.       36: return  



0-15行的字节码是准备动作,作用是建立man和woman的内存空间,调用Man和Woman类的实例构造器,将这两个实例的引用存放在第1和第2个局部变量表Slot之中,这个动作对应了代码中这两句:

[java] view plaincopy在CODE上查看代码片派生到我的代码片
 
  1. Human man = new Man();  
  2. 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-11:Java11OCP学习笔记

    积分java源码Java 11 Java SE ...表达式由变量、运算符和方法调用组成。 表达式计算为单个值。 表达式是计算值的东西,而语句是做某事的一行代码。 某些表达式可以通过以分号结尾的方式组成语句,例

    java7rt.jar源码-Java_JVM:这是我的JavaJVM学习笔记

    Runtime或System类调用exit()方法或Runtime调用half()方法 JVM的框架: 执行引擎: (字节)解释器 + JIT(java即时编译器) 前者是用 PC计数器 来依次编译每一行代码解释为本地机器指令; 后者是通过 寻找热点代码 进行...

    Scala学习笔记

    Scala运行于Java平台(Java虚拟机),并兼容现有的Java程序。它也能运行于CLDC配置的Java ME中。目前还有另一.NET平台的实现[2],不过该版本更新有些滞后。[3] Scala的编译模型(独立编译,动态类加载)与Java和C#...

    day020-继承加强和设计模式代码和笔记.rar

    控制创建对象的数量 =&gt; 创建对象通过new 调用构造方法 =&gt; 控制构造方法就能控制创建对象 控制调用构造方法 =&gt; 用private修饰 =&gt; 需要给外部提供一个对象 =&gt; 先在类中创建一个对象 (联想到封装) =&gt; ...

    java8rt.jar源码-JVM:学习JVM

    2.java虚拟机栈:线程私有,虚拟机栈描述的是java方法执行的内存模型:每个方法在执行的时候都会创建一个栈帧,存储局部变量表、操作数栈、动态链接、方法出口等信息。每一个 方法从调用到执行完成的过程中,就对应...

    java版斗地主源码-MyJava:记录我自己Java学习的一些笔记,才疏学浅还望多多指教

    java版斗地主源码 [toc] :star:JAVA基础 面向对象和面向过程的区别 面向过程 :面向过程性能比面向对象高。 因为类调用时需要实例化,开销比较大,比较...Java虚拟机(JVM)是运行 Java 字节码的虚拟机。保证 了java

    c#学习笔记.txt

    如果没有ref,out则默认为值传递,虽然可以在方法中修改这个参数的值,但是修改后的值不会还会到调用该方法的程序中. params :params 关键字可以指定在参数数目可变处采用参数的方法参数 ref :引用传递 out : 7, ...

    Google Android SDK开发范例大全(完整版)

    Android 应用程序是用 Java 语言编写的,但是是在 Dalvik VM(非 Java 虚拟机)中编译和执行的。在 Eclipse 中用 Java 语言编程非常简单;Eclipse 提供一个丰富的 Java 环境,包括上下文敏感帮助和代码提示。Java ...

    Eclipse开发分布式商城系统+完整视频代码及文档

    │ │ 深入理解Java内存模型.pdf │ │ │ └─课后资料 │ ├─笔记 │ │ 淘淘商城_day20_课堂笔记.docx │ │ │ └─视频 │ 07-使用Jedis连接集群操作.avi │ 00-今日大纲.avi │ 01-RDB持久化方式.avi │ 02...

    android笔记.rar

    3.3 Java对C库的调用 ... ...70 3.3.1 android中使用JNI... ..70 3.3.2 安装使用NDK ... .72 3.3.3 在源码中将库打进apk ... ...73 3.3.4 简单的C库调试方法 ... ...75 3.4 典型应用... ..76 3.4.1 语音合成... .76 ...

    2010年谢彦的android笔记

    3.3 Java对C库的调用 70 3.3.1 android中使用JNI 70 3.3.2 安装使用NDK 72 3.3.3 在源码中将库打进apk 73 3.3.4 简单的C库调试方法 75 3.4 典型应用 76 3.4.1 语音合成 76 3.4.2 语音识别简介 79 3.4.3 语音识别方法...

    【读书笔记】【图解JVM】

    JVM内在结构的图解文档,visio格式 1.编译过程 2.内存结构 3.对象与类的结构 4.类结构信息 5.对象建立过程 6.收集器 7.方法调用 8.垃圾回收 9.JIT 10.指令 11.线程

    koboapplet:Kobo 实用程序从 SQLite 数据库中提取信息

    除了将对象导出到 Javascript 虚拟机环境的 Java API 和 Java Applet 之外,我们还有一个 JavaScript 包装层来帮助调用 Java 方法并将其包装在 Javascript 对象中。 ##API java web start 应用程序作为一个例子,也...

    香槟网络系统 G H O S T XP SP3 7.0

    *破解 Tcpip 连接数限制,破解系统主题限制,并集成微软 JAVA 虚拟机。 *集成 DirectX0903,VBVC最新版本运行库。 *系统在完全断网的情况下制作,确保系统更安全。 *采用通过微软数字签名认证驱动并自动识别安装,...

    APKTool批处理版l

    第二种方法,就在res目录里面建立对应的语言资源文件夹(简体中文资源的目录名是values-zh-rCN,繁体中文是values-zh-rTW),将英文资源values里面的arrays.xml和strings.xml复制到新目录里面进行汉化,让Android...

Global site tag (gtag.js) - Google Analytics