深入虚拟机笔记之方法调用和返回

第19章 方法调用和返回

    当java虚拟机调用一个类方法时,它会基于对象引用的类型(编译时可知)来选择所调用的方法;相反,当虚拟机调用一个实例方法时,它会基于对象实际的类(只能在运行时得知)来选择所调用的方法。

    最初,所有的调用指令都指向一个包含符号引用的常量池入口。当java虚拟机遇到一条调用指令时,如果还没有解析符号引用,那么虚拟机把符号引用作为执行指令调用执行过程的一部分。要解析一个符号引用,java虚拟机要确定被符号化引用的方法,然后再用一个直接引用来代替符号引用。直接引用就如同偏移量指针一样,如果将来再次使用该引用,它可以使虚拟机更快地调用这个方法。

    在解析过程中,java虚拟机还将执行几次确认检验,以确保遵循java语言的规法和调用invoke指令的安全。一旦解析了一个方法后,java虚拟机就准备调用它;如果这个方法是一个实例方法,它必须在一个对象中被调用;对实例方法的调用,虚拟机需要在栈里存在一个对象引用(objectref);如果方法需要参数,那么虚拟机还需要在栈中存在该方法所需要的参数(args)。如果这个方法是一个类方法,虚拟机只需要栈中存在args参数。objectref和args必须在调用指令执行前,被其他指令压入所调用方法的操作数栈。

    虚拟机为每一个调用的java方法(非本地方法)建立一个新的栈帧。栈帧包括:为方法的局部变量所预留的空间、该方法的操作数栈以及特定虚拟机实现需要的其他所有信息。局部变量表和操作数栈的大小在编译时计算出来,并放置到class文件中去。虚拟机借此可以了解方法的栈帧需要多少内存;当它调用一个方法时,它为该方法创建恰当大小的栈帧,再将新的栈帧压入java栈。

    处理实例方法时,虚拟机把所调用方法栈帧中的操作数栈中弹出objectref和args。虚拟机把objectref作为局部变量0放到新的栈帧中,把所有的args作为局部变量1、2 、。。。objectref的值是隐式传递给所有实例方法的this指针。对于类方法,虚拟机只弹出参数,并将它们放到局部变量的0、1、2、。。。然后虚拟机把新的栈帧作为当前栈帧,并将PC寄存器(程序计数器)指向方法的第一条指令。

    尽管通常使用invokevirtual指令调用实例方法,但在某些特定的情况中,也会使用另外两种操作码 -- invokespecial和invokeinterface。

    java虚拟机总是直接调用类初始化方法(<clinit>()),类的初始化方法永远不会被任何字节码调用。在java虚拟机的指令集中,没人任何调用<clinit>()方法的指令。如果class文件尝试使用任何指令调用<clinit>()方法,会导致虚拟机抛出异常。

    invokespecial和invokevirtual的主要区别在于:invokespecail通常根据引用的类型来选择方法,而不是根据对象的类型来选择。它使用静态(编译时)绑定而不是动态(运行时)绑定。

    当根据引用的类型来调用实例方法,而不是根据对象的类来调用的时候,通常使用invokespecial指令,分为3种情况:

    (1) 实例初始化方法(<init>())。

    (2) 私有方法。

    (3) 使用super关键字所调用的方法。

    invokespecial调用<init>()方法:<init>()方法是编译器为构造方法和实例变量初始化放置代码的地方。class文件中,类会为每个构造方法提供一个 <init>()方法。就像每个类都至少会有一个构造方法一样,每个类都至少会有一个 <init>()方法,这些方法通常使用invokespecial指令调用。

    只有创建一个新的实例的时候,才调用 <init>()方法。新创建对象的继承路径中,每个超类都至少会调用一个 <init>()方法。使用invokespecial指令调用 <init>()方法的原因在于,子类的 <init>()方法需要拥有调用超类的 <init>()方法的能力。当一个对象实例化时,虚拟机调用类中声明的 <init>()方法,这个 <init>()方法首先调用同一个类中的其他 <init>()方法或者超类的 <init>()方法,这个过程贯穿于对象的整个生命周期。

    invokespecial调用私有方法:当使用 invokespecial调用私有方法时,虚拟机会按照引用的类型来选择调用的方法。

    invokespecial和super关键字:指令invokevirtual只能调用当前类的方法,无法使用超类的方法。java虚拟机是否使用静态绑定来执行invokespecial指令(或者使用特殊的动态绑定)取决于所执行的类是否设定了ACC_SUPER标志。在jdk1.0.2版本以前,invokespecial指令的名称为invokenonvirtual,而且总会导致静态绑定的使用,结果是无法保证所有情况下的java语言语义的正确实现(指令集中的一个bug)。在jdk1.0.2版本中,invokenonvirtual指令更名为invokespecial,它的语义也改变了。此外,java class文件中的access_flags项中还加入了一个新的标志:ACC_SUPER。class文件的ACC_SUPER标志指明,java虚拟机使用哪一种语义来执行class文件中遇到的invokespecial指令。如果没有设置ACC_SUPER标志,虚拟机将会使用旧的语义(invokenonvirtual语义);如果设置了ACC_SUPER标志,虚拟机将使用新的语义。

    invokespecial新的语义除了调用超类方法之外,其他情况一律使用静态绑定。当java虚拟机解析一个invokespecial指令中指向超类方法的符号引用时,它会动态搜寻当前类的超类,找到离得最近的超类中的该方法的实现。大多数情况下,虚拟机很可能发现最近的方法实现存在于符号引用中列出的超类中。另外一种情况是:子类编译后,继承结构中的某个超类实现发生了变化,而子类没有重新进行编译;此时子类的class文件中的符号引用会指向超类修改前的实现类,invokespecial新语义在执行时会动态进行搜索,以确保语义的正确性。

    invokeinterface和invokevirtual的功能相同, 这两条指令的区别在于:当引用的类型为类的时候,使用invokevirtual;当引用的类型为接口时,使用invokeinterface指令。java虚拟机使用不同于类引用的操作码来调用接口引用的方法,这是因为java不能像使用类引用那样,使用许多与方法表偏移量相关的假设。对于类引用来说,无论对象实际的类是什么,方法在方法表中始终占据相同的位置。但对于接口引用来说,由于实现同一接口的类,可能扩展(extends)了不同的超类或者实现了其他的接口,位于不同类中的同一方法所占据的位置是不同的。

    调用接口引用方法可能要比调用类引用方法慢。因为,当java虚拟机遇到invokevirtual指令时,它把实例方法的符号引用解析为直接引用,所生成的直接引用很可能是方法表中的一个偏移量,而且从此往后都可以使用同样的偏移量。但对于invokeinterface指令,虚拟机每一次遇到invokeinterface指令,都不得不重新搜索一遍方法表,因为虚拟机不能够假设这一次的偏移量与上一次的偏移量相同。

    最快的指令是invokespecial和invokestatic,当java虚拟机为这些指令解析符号引用时,将符合引用转换为直接引用,所生成的直接引用将包含一个指向实际操作码的指针。

    从方法中返回:每一种操作码对应一种返回的数据类型,它们都没有操作数,如果有返回值,必须被放置在操作数栈中。返回值从操作数栈中弹出,然后被压入调用方法(调用代码所在的方法)的栈帧的操作数栈中。弹出当前栈帧,调用方法的栈帧成为当前栈帧;程序计数器被重置,指向紧随调用返回方法那条指令的下一条指令。指令ireturn用于返回int、char、byte和short类型数据。

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

    invokespecial的例外情况:调用超类方法时,class中保存的时编译时的超类方法类型,运行时,如果超类中的继承结构变化,将会动态使用相应的方法版本。

猜你喜欢

转载自jaesonchen.iteye.com/blog/2289797