方法调用 深入理解Java虚拟机 总结

        方法调用并不等同于方法执行,方法调用阶段唯一的任务就是确定被调用方法的版本(即调用哪一个方法),暂时还不涉及方法内部的具体运行过程。在程序运行时,进行方法调用是最普遍、最频繁的操作,但前面已经讲过,Class 文件的编译过程中不包含传统编译中的连接步骤,一切方法调用在 Class 文件里面存储的都只是符号引用,而不是方法在实际运行时内存布局中的入口地址(相当于之前说的直接引用)。这个特性给 Java 带来了更强大的动态扩展能力,但也使得 Java 方法调用过程变得相对复杂起来,需要在类加载期间,甚至到运行期间才能确定目标方法的直接引用。

解析

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

        在 Java 语言中符合 “编译期可知,运行期不可变” 这个要求的方法,主要包括静态方法和私有方法两大类,前者与类型直接关联,后者在外部不可被访问,这两种方法各自的特点决定了它们都不可能通过继承或别的方式重写其他版本,因此它们都适合在类加载阶段进行解析。

        与之相对应的是,在 Java 虚拟机里面提供了 5 条方法调用字节码指令,分别如下。

invokestatic:调用静态方法。

invokespecial:调用实例构造器 方法、私有方法和父类方法。

invokevirtual::调用所有的虚方法。

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

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

        只要能被 invokestatic 和 invokespecial 指令调用的方法,都可以在解析阶段确定唯一的调用版本,符合这个条件的有静态方法、私有方法、实例构造器、父类方法 4 类,它们在类加载的时候就会把符号引用解析为该方法的直接引用。这些方法可以称为非虚方法,与之相反,其他方法称为虚方法(除去 final 方法,后文会提到)。



分派

        众所周知,Java 是一门面向对象的程序语言,因为 Java 具备面向对象的 3 个基本特征:继承、封装和多态。本节讲解的分派调用过程将会揭示多态性特征的一些最基本的体现,如 “重载” 和 “重写” 在 Java 虚拟机之中是如何实现的,这里的实现当然不是语法上该如何写,我们关心的依然是虚拟机如何确定正确的目标方法。

1.静态分派

        在开始讲解静态分派(严格来说,Dispatch 这个词一般不用再静态环境中,英文技术文档的称呼是 “Method Overload Resolution”,但国内的各种资料都普遍将这种行为翻译成 “静态分派”,特此说明)。

Human man = new Man();  

        我们把上面代码中的 “Human” 称为变量的静态类型(Static Type),或者叫做外观类型(Apparent Type),后面的 “Man” 则称为变量的实际类型(Actual Type),静态类型和实际类型在程序中都可以发生一些变化,区别是静态类型的变化仅仅在使用时发生,变量本身的静态类型不会被改变,并且最终的静态类型是在编译期可知的;而实际类型变化的结果在运行期才可确定,编译器在编译程序的时候并不知道一个对象的实际类型是什么。虚拟机(准确地说是编译器)在重载时是通过参数的静态类型而不是实际类型作为判定依据的。 

         所有依赖静态类型来定位方法执行版本的分派动作称为静态分派。静态分派的典型应用是方法重载。静态分派发生在编译阶段,因此确定静态分派的动作实际上不是由虚拟机来执行的。另外,编译器虽然能确定出方法的重载版本,但在很多情况下这个重载版本并不是 “唯一的”,往往只能确定一个 “更加适合的” 版本。这种模糊的结论在由 0 和 1 构成的计算机世界中算是比较 “稀罕” 的事情,产生这种模糊结论的主要原因是字面量不需要定义,所以字面量没有显式的静态类型,它的静态类型只能通过语言上的规则去理解和推断。

         另外还有一点读者可能比较容易混淆:笔者讲述的解析与分派这两者之间的关系并不是二选一的排他关系,它们是不同层次上去筛选、确定目标方法的过程。例如,前面说过,静态方法会在类加载期就进行解析,而静态方法显然也是可以拥有重载版本的,选择重载版本的过程也是通过静态分派完成的。

2.动态分派

        了解了静态分派,我们接下来看一下动态分派的过程,它和动态性的另外一个重要体现(注:有一种观点认为:因为重载是静态的,重写是动态的,所以只有重写算是多态性的体现,重载不算多态。笔者认为这种整理没有意义,概念仅仅是说明问题的一种工具而已)——重写(Override)有着密切的关联。在运行期根据实际类型确定方法执行版本的分派过程称为动态分派

3.单分派与多分派

         方法的接收者与方法的参数统称为方法的宗量,这个定义最早应该来源于《Java 与模式》一书。根据分派基于多少种宗量,可以将分派划分为单分派和多分派两种。单分派是根据一个宗量对目标方法进行选中,多分派则根据多于一个宗量对目标方法进行选择。

        单分派和多分派的定义读起来拗口,从字面上看也比较抽象,不过对照实例看就不难理解了。代码清单 8-10 中列举了一个 Father 和 Son 一起来做出 “一个艰难的决定” 的例子。


        在 main 函数中调用了两次 hardChoice() 方法,这两次 hardChoice() 方法的选择结果在程序输出中已经显示得很清楚了。

        我们来看看编译阶段编译器的选择过程,也就是静态分派的过程。这时选择目标方法的依据有两点:一是静态类型是 Father 还是 Son,二是方法参数是 QQ 还是 360。这次选择结果的最终产物是产生了两条 invokevirtual 指令,两条指令的参数分别为常量池中指Father.hardChoice(360) 及 Father.hardChoice(QQ) 方法的符号引用。因为是根据两个宗量进行选择,所以Java 语言的静态分派属于多分派类型

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

        根据上述论证的结果,我们可以总结依据:今天的 Java 语言是一门静态多分派、动态单分派的语言。强调 “今天的 Java 语言” 是因为这个结论未必会恒久不变,C# 在 3.0 及之前的版本与 Java 一眼的动态单分派语言,但在 C# 4.0 中引入了 dynamic 类型后,就可以很方便地实现动态多分派。

        按照目前 Java 语言的发展趋势,它并没有直接变为动态语言的迹象,而是通过内置动态语言(如 JavaScript)执行引擎的方式来满足动态性的需求。但是 Java 虚拟机层面上则不是如此,在 JDK 1.7 中实现的 JSR-292 里面就已经开始提供对动态语言的支持了,JDK 1.7 中新增的 invokedynamic 指令也成为了最复杂的一条方法调用的字节码指令。


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

        前面介绍的分派过程,作为对虚拟机概念模型的解析基本上已经足够了,它已经解决了虚拟机在分派中 “会做什么” 这个问题。但是虚拟机 “具体是如何做到的”,可能各种虚拟机的实现都会有些差别。

        由于动态分派是非常频繁的动作,而且动态分派的方法版本选择过程需要运行时在类的方法元数据中搜索合适的目标方法,因此在虚拟机的实际实现中基于性能的考虑,大部分实现都不会真正地进行如此频繁的搜索。面对这种情况,最常用的 “稳定优化” 手段就是为类在方法区中建立一个虚方法表(Virtual Method Table,也称为 vtable,与此对应的,在 invokeinterface 执行时也会用到接口方法表——Interface Method Table,简称 itable),使用虚方法表索引来代替元数据查找以提高性能。我们先看看但清单 8-10 所对应的虚方法表结构示例,如图 8-3 所示。

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

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

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

        上文中笔者说方法表是分派调用的 “稳定优化” 手段,虚拟机除了使用方法表之外,在条件允许的情况下,还会使用内联缓存(Inline Cache)和基于 “类型继承关系分析”(Class Hierarchy Analysis,CHA)技术的守护内联(Guarded Inlining)两种非稳定的 “激进优化” 手段来获得更高的性能。


动态类型语言支持

        Java 虚拟机的字节码指令集的数量从 Sun 公司的第一款 Java 虚拟机问世至 JDK 7 来临之前的十余年时间里,一直没有发生任何变化。随着 JDK 7 的发布,字节码指令集终于迎来了第一位新成员——invokedynamic 指令。这条新增加的指令是 JDK 7 实现 “动态类型语言” (Dynamically Typed Language)支持而进行的改进之一,也是为 JDK 8 可以顺序实现 Lambda 表达式做技术准备。在本节中,我们将详细讲解 JDK 7 这项新特性出现的前因后果和它的深远意义。

1. 动态类型语言

        在介绍 Java 虚拟机的动态类型语言支持之前,我们要先弄明白动态类型语言是什么?它与 Java 语言、Java 虚拟机有什么关系?了解 JDK 1.7 提供动态类型语言支持的技术背景,对理解这个语言特性是很有必要的。

        什么是动态类型语言(注意:动态类型语言与动态语言、弱类型语言并不是一个概念,需要区别对待)?动态类型语言的关键特征是它的类型检查的主体过程是在运行期而不是编译期,满足这个特征的语言有很多,常用的包括:APL、Clojure、Erlang、Groovy、JavaScript、Jython、Lisp、Lua、PHP、Prolog、Python、Ruby、Smalltalk 和 Tcl 等。相对的,在编译期就进行类型检查过程的语言(如 C++ 和 Java 等)就是最常用的静态类型语言。

        “变量无类型而变量值才有类型” 这个特点也是动态类型语言的一个重要特征。静态类型语言在编译期确定类型,最显著的好处是编译器可以提供严谨的类型检查,这样与类型相关的问题能在编码的时候就及时发现,利于稳定性及代码达到更大规模。而动态类型语言在运行期确定类型,这可以为开发人员提供更大的灵活性,某些在静态类型语言中需用大量 “臃肿” 代码来实现的功能,由动态类型语言来实现可能会更加清晰和简洁,清晰和简洁通常也就意味着开发效率的提升。


最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 199,440评论 5 467
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 83,814评论 2 376
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 146,427评论 0 330
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 53,710评论 1 270
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 62,625评论 5 359
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,014评论 1 275
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,511评论 3 390
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,162评论 0 254
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,311评论 1 294
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,262评论 2 317
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,278评论 1 328
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,989评论 3 316
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,583评论 3 303
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,664评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,904评论 1 255
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,274评论 2 345
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 41,856评论 2 339

推荐阅读更多精彩内容