宁波网站建设企业,林业网站模板,网站规划与设计大作业,深圳市住房与建设局招聘方法调用并不等同于方法执行#xff0c;方法调用阶段唯一的任务就是确定被调用方法的版本#xff08;即调用哪一个方法#xff09;#xff0c;暂时还不涉及方法内部的具体运行过程。在程序运行时#xff0c;进行方法调用是最普遍、最频繁的操作#xff0c;Class文件的编译…方法调用并不等同于方法执行方法调用阶段唯一的任务就是确定被调用方法的版本即调用哪一个方法暂时还不涉及方法内部的具体运行过程。在程序运行时进行方法调用是最普遍、最频繁的操作Class文件的编译过程中不包含传统编译中的连接步骤一切方法调用在Class文件里面存储的都只是符号引用而不是方法在实际运行时内存布局中的入口地址相当于之前说的直接引用。这个特性给Java带来了更强大的动态扩展能力但也使得Java方法调用过程变得相对复杂起来需要在类加载期间甚至到运行期间才能确定目标方法的直接引用。 解析调用一定是个静态的过程在编译期间就完全确定在类装载的解析阶段就会把涉及的符号引用全部转变为可确定的直接引用不会延迟到运行期再去完成。而分派Dispatch调用则可能是静态的也可能是动态的根据分派依据的宗量数可分为单分派和多分派。这两类分派方式的两两组合就构成了静态单分派、静态多分派、动态单分派、动态多分派4种分派组合情况。 解析 所有方法调用中的目标方法在Class文件里面都是一个常量池中的符号引用在类加载的解析阶段会将其中的一部分符号引用转化为直接引用这种解析能成立的前提是方法在程序真正运行之前就有一个可确定的调用版本并且这个方法的调用版本在运行期是不可改变的。换句话说调用目标在程序代码写好、编译器进行编译时就必须确定下来。这类方法的调用称为解析Resolution。在Java语言中符合“编译期可知运行期不可变”这个要求的方法主要包括静态方法和私有方法两大类前者与类型直接关联后者在外部不可被访问这两种方法各自的特点决定了它们都不可能通过继承或别的方式重写其他版本因此它们都适合在类加载阶段进行解析。 与之相对应的是在Java虚拟机里面提供了5条方法调用字节码指令分别如下。 invokestatic调用静态方法。 invokespecial调用实例构造器init方法、私有方法和父类方法。 invokevirtual调用所有的虚方法。 invokeinterface调用接口方法会在运行时再确定一个实现此接口的对象。 invokedynamic先在运行时动态解析出调用点限定符所引用的方法然后再执行该方法在此之前的4条调用指令分派逻辑是固化在Java虚拟机内部的而invokedynamic指令的分派逻辑是由用户所设定的引导方法决定的。 只要能被invokestatic和invokespecial指令调用的方法都可以在解析阶段中确定唯一的调用版本符合这个条件的有静态方法、私有方法、实例构造器、父类方法4类它们在类加载的时候就会把符号引用解析为该方法的直接引用。这些方法可以称为非虚方法与之相反其他方法称为虚方法除去final方法。 Java中的非虚方法除了使用invokestatic、invokespecial调用的方法之外还有一种就是被final修饰的方法。虽然final方法是使用invokevirtual指令来调用的但是由于它无法被覆盖没有其他版本所以也无须对方法接收者进行多态选择又或者说多态选择的结果肯定是唯一的。在Java语言规范中明确说明了final方法是一种非虚方法。 分派 Java是一门面向对象的程序语言因为Java具备面向对象的3个基本特征继承、封装和多态。分派调用过程将会揭示多态性特征的一些最基本的体现如“重载”和“重写”在Java虚拟机之中是如何实现的这里的实现当然不是语法上该如何写我们关心的依然是虚拟机如何确定正确的目标方法。 静态分派 public class StaticDispatch{static abstract class Human{}static class Man extends Human{}static class Woman extends Human{}public void sayHelloHuman guy{System.out.printlnhello,guy}public void sayHelloMan guy{System.out.printlnhello,gentleman}public void sayHelloWoman guy{System.out.printlnhello,lady}public static void mainString[]args{Human mannew ManHuman womannew WomanStaticDispatch srnew StaticDispatchsr.sayHellomansr.sayHellowoman}
}hello,guy
hello,guy main里面的两次sayHello方法调用在方法接收者已经确定是对象“sr”的前提下使用哪个重载版本就完全取决于传入参数的数量和数据类型。代码中刻意地定义了两个静态类型相同但实际类型不同的变量但虚拟机准确地说是编译器在重载时是通过参数的静态类型而不是实际类型作为判定依据的。并且静态类型是编译期可知的因此在编译阶段Javac编译器会根据参数的静态类型决定使用哪个重载版本所以选择了sayHelloHuman作为调用目标并把这个方法的符号引用写到main方法里的两条invokevirtual指令的参数中。所有依赖静态类型来定位方法执行版本的分派动作称为静态分派。静态分派的典型应用是方法重载。静态分派发生在编译阶段因此确定静态分派的动作实际上不是由虚拟机来执行的。另外编译器虽然能确定出方法的重载版本但在很多情况下这个重载版本并不是“唯一的”往往只能确定一个“更加合适的”版本。这种模糊的结论在由0和1构成的计算机世界中算是比较“稀罕”的事情产生这种模糊结论的主要原因是字面量不需要定义所以字面量没有显式的静态类型它的静态类型只能通过语言上的规则去理解和推断。何为“更加合适的”版本。 public class Overload{public static void sayHelloObject arg{System.out.printlnhello Object}public static void sayHelloint arg{System.out.printlnhello int}public static void sayHellolong arg{System.out.printlnhello long}public static void sayHelloCharacter arg{System.out.printlnhello Character}public static void sayHellochar arg{System.out.printlnhello char}public static void sayHellochar……arg{System.out.printlnhello char……}public static void sayHelloSerializable arg{System.out.printlnhello Serializable}public static void mainString[]args{sayHelloa}
} 上面的代码运行后会输出 hello char 这很好理解a是一个char类型的数据自然会寻找参数类型为char的重载方法如果注释掉sayHellochar arg方法那输出会变为 hello int 这时发生了一次自动类型转换a除了可以代表一个字符串还可以代表数字97字符a的Unicode数值为十进制数字97因此参数类型为int的重载也是合适的。我们继续注释掉sayHelloint arg方法那输出会变为 hello long 这时发生了两次自动类型转换a转型为整数97之后进一步转型为长整数97L匹配了参数类型为long的重载。笔者在代码中没有写其他的类型如float、double等的重载不过实际上自动转型还能继续发生多次按照char-int-long-float-double的顺序转型进行匹配。但不会匹配到byte和short类型的重载因为char到byte或short的转型是不安全的。我们继续注释掉sayHellolong arg方法那输出会变为 hello Character 这时发生了一次自动装箱a被包装为它的封装类型java.lang.Character所以匹配到了参数类型为Character的重载继续注释掉sayHelloCharacter arg方法那输出会变为 hello Serializable 这个输出可能会让人感觉摸不着头脑一个字符或数字与序列化有什么关系出现hello Serializable是因为java.lang.Serializable是java.lang.Character类实现的一个接口当自动装箱之后发现还是找不到装箱类但是找到了装箱类实现了的接口类型所以紧接着又发生一次自动转型。char可以转型成int但是Character是绝对不会转型为Integer的它只能安全地转型为它实现的接口或父类。Character还实现了另外一个接口java.lang.ComparableCharacter如果同时出现两个参数分别为Serializable和ComparableCharacter的重载方法那它们在此时的优先级是一样的。编译器无法确定要自动转型为哪种类型会提示类型模糊拒绝编译。程序必须在调用时显式地指定字面量的静态类型如sayHelloComparableCharactera才能编译通过。下面继续注释掉sayHelloSerializable arg方法输出会变为 hello Object 这时是char装箱后转型为父类了如果有多个父类那将在继承关系中从下往上开始搜索越接近上层的优先级越低。即使方法调用传入的参数值为null时这个规则仍然适用。我们把sayHelloObject arg也注释掉输出将会变为 hello char…… 7个重载方法已经被注释得只剩一个了可见变长参数的重载优先级是最低的这时候字符a被当做了一个数组元素。使用的是char类型的变长参数在验证时还可以选择int类型、Character类型、Object类型等的变长参数重载来把上面的过程重新演示一遍。但要注意的是有一些在单个参数中能成立的自动转型如char转型为int在变长参数中是不成立的。 另外还有一点可能比较容易混淆解析与分派这两者之间的关系并不是二选一的排他关系它们是在不同层次上去筛选、确定目标方法的过程。例如前面说过静态方法会在类加载期就进行解析而静态方法显然也是可以拥有重载版本的选择重载版本的过程也是通过静态分派完成的。 动态分派 public class DynamicDispatch{static abstract class Human{protected abstract void sayHello}static class Man extends Human{Overrideprotected void sayHello{System.out.printlnman say hello}}static class Woman extends Human{Overrideprotected void sayHello{System.out.printlnwoman say hello}}public static void mainString[]args{Human mannew ManHuman womannew Womanman.sayHellowoman.sayHellomannew Womanman.sayHello}
}
运行结果
man say hello
woman say hello
woman say hello 虚拟机是如何知道要调用哪个方法的显然这里不可能再根据静态类型来决定因为静态类型同样都是Human的两个变量man和woman在调用sayHello方法时执行了不同的行为并且变量man在两次调用中执行了不同的方法。导致这个现象的原因很明显是这两个变量的实际类型不同Java虚拟机是如何根据实际类型来分派方法执行版本的呢 main方法的字节码
public static void mainjava.lang.String[]
Code
Stack2Locals3Args_size1
0new#16//class org/fenixsoft/polymorphic/Dynamic-Dispatch $Man
3dup
4invokespecial#18//Method org/fenixsoft/polymorphic/Dynamic-Dispatch $Man.initV
7astore_1
8new#19//class org/fenixsoft/polymorphic/Dynamic-Dispatch $Woman
11dup
12invokespecial#21//Method org/fenixsoft/polymorphic/DynamicDispatch $Woman.initV
15astore_2
16aload_1
17invokevirtual#22//Method org/fenixsoft/polymorphic/Dynamic-Dispatch $Human.sayHelloV
20aload_2
21invokevirtual#22//Method org/fenixsoft/polymorphic/Dynamic-Dispatch $Human.sayHelloV
24new#19//class org/fenixsoft/polymorphic/Dynamic-Dispatch $Woman
27dup
28invokespecial#21//Method org/fenixsoft/polymorphic/DynamicDispatch $Woman.initV
31astore_1
32aload_1
33invokevirtual#22//Method org/fenixsoft/polymorphic/DynamicDispatch $Human.sayHelloV
36return
015行的字节码是准备动作作用是建立man和woman的内存空间、调用Man和Woman类型的实例构造器将这两个实例的引用存放在第1、2个局部变量表Slot之中这个动作也就对应了代码中的这两句
Human man newMan
Human woman newWoman 接下来的1621句是关键部分16、20两句分别把刚刚创建的两个对象的引用压到栈顶这两个对象是将要执行的sayHello方法的所有者称为接收者Receiver17和21句是方法调用指令这两条调用指令单从字节码角度来看无论是指令都是invokevirtual还是参数都是常量池中第22项的常量注释显示了这个常量是Human.sayHello的符号引用完全一样的但是这两句指令最终执行的目标方法并不相同。 原因就需要从invokevirtual指令的多态查找过程开始说起invokevirtual指令的运行时解析过程大致分为以下几个步骤 1找到操作数栈顶的第一个元素所指向的对象的实际类型记作C。 2如果在类型C中找到与常量中的描述符和简单名称都相符的方法则进行访问权限校验如果通过则返回这个方法的直接引用查找过程结束如果不通过则返回java.lang.IllegalAccessError异常。 3否则按照继承关系从下往上依次对C的各个父类进行第2步的搜索和验证过程。 4如果始终没有找到合适的方法则抛出java.lang.AbstractMethodError异常。 由于invokevirtual指令执行的第一步就是在运行期确定接收者的实际类型所以两次调用中的invokevirtual指令把常量池中的类方法符号引用解析到了不同的直接引用上这个过程就是Java语言中方法重写的本质。我们把这种在运行期根据实际类型确定方法执行版本的分派过程称为动态分派。 单分派与多分派 方法的接收者与方法的参数统称为方法的宗量这个定义最早应该来源于《Java与模式》一书。根据分派基于多少种宗量可以将分派划分为单分派和多分派两种。单分派是根据一个宗量对目标方法进行选择多分派则是根据多于一个宗量对目标方法进行选择。 列举了一个Father和Son一起来做出“一个艰难的决定”的例子。 public class Dispatch{static class QQ{}static class_360{}public static class Father{public void hardChoiceQQ arg{System.out.printlnfather choose qq}public void hardChoice_360 arg{System.out.printlnfather choose 360}}public static class Son extends Father{public void hardChoiceQQ arg{System.out.printlnson choose qq}public void hardChoice_360 arg{System.out.printlnson choose 360}}public static void mainString[]args{Father fathernew FatherFather sonnew Sonfather.hardChoicenew_360son.hardChoicenew QQ}
}
运行结果
father choose 360
son choose qq 在main函数中调用了两次hardChoice方法这两次hardChoice方法的选择结果在程序输出中已经显示得很清楚了。 我们来看看编译阶段编译器的选择过程也就是静态分派的过程。这时选择目标方法的依据有两点一是静态类型是Father还是Son二是方法参数是QQ还是360。这次选择结果的最终产物是产生了两条invokevirtual指令两条指令的参数分别为常量池中指向Father.hardChoice360及Father.hardChoiceQQ方法的符号引用。因为是根据两个宗量进行选择所以Java语言的静态分派属于多分派类型。 再看看运行阶段虚拟机的选择也就是动态分派的过程。在执行“son.hardChoicenew QQ”这句代码时更准确地说是在执行这句代码所对应的invokevirtual指令时由于编译期已经决定目标方法的签名必须为hardChoiceQQ虚拟机此时不会关心传递过来的参数“QQ”到底是“腾讯QQ”还是“奇瑞QQ”因为这时参数的静态类型、实际类型都对方法的选择不会构成任何影响唯一可以影响虚拟机选择的因素只有此方法的接受者的实际类型是Father还是Son。因为只有一个宗量作为选择依据所以Java语言的动态分派属于单分派类型。 虚拟机动态分派的实现 由于动态分派是非常频繁的动作而且动态分派的方法版本选择过程需要运行时在类的方法元数据中搜索合适的目标方法因此在虚拟机的实际实现中基于性能的考虑大部分实现都不会真正地进行如此频繁的搜索。面对这种情况最常用的“稳定优化”手段就是为类在方法区中建立一个虚方法表Vritual Method Table也称为vtable与此对应的在invokeinterface执行时也会用到接口方法表——Inteface Method Table简称itable使用虚方法表索引来代替元数据查找以提高性能。 虚方法表中存放着各个方法的实际入口地址。如果某个方法在子类中没有被重写那子类的虚方法表里面的地址入口和父类相同方法的地址入口是一致的都指向父类的实现入口。如果子类中重写了这个方法子类方法表中的地址将会替换为指向子类实现版本的入口地址。Son重写了来自Father的全部方法因此Son的方法表没有指向Father类型数据的箭头。但是Son和Father都没有重写来自Object的方法所以它们的方法表中所有从Object继承来的方法都指向了Object的数据类型。 为了程序实现上的方便具有相同签名的方法在父类、子类的虚方法表中都应当具有一样的索引序号这样当类型变换时仅需要变更查找的方法表就可以从不同的虚方法表中按索引转换出所需的入口地址。方法表一般在类加载的连接阶段进行初始化准备了类的变量初始值后虚拟机会把该类的方法表也初始化完毕。 ps:方法表是分派调用的“稳定优化”手段虚拟机除了使用方法表之外在条件允许的情况下还会使用内联缓存Inline Cache和基于“类型继承关系分析”Class Hierarchy Analysis,CHA技术的守护内联Guarded Inlining两种非稳定的“激进优化”手段来获得更高的性能关于这两种优化技术的原理和运作过程可以参考JIT晚期运行期。 转载于:https://www.cnblogs.com/wade-luffy/p/6058075.html