《自己动手写Java虚拟机》学习笔记(七)

第七章 方法调用和返回

本章将实现方法调用和返回。还会讨论类和对象的初始化。

7.1 方法调用概述

从调用的角度来看,方法可以分为两类,静态方法(或类方法)和实例方法。静态方法通过类来调用,实例方法则通过对象引用来调用。静态方法是静态绑定的,也就是说,最终调用的是哪个方法在编译期就已经确定;而实例方法支持动态绑定,最终要调用哪个方法需要到了运行期才知道。

从实现角度,可以分为:抽象方法,Java(或者JVM上的其他语言)实现和本地语言(如C/C++)实现。

Java7之前,有四条方法调用指令:invokestatic指令用来调用静态方法。invokespecial指令用来调用无需动态绑定的实例方法,包括构造函数、私有方法和通过super关键字调用的超类方法。剩下的是动态绑定。如果针对接口类型的引用调用方法,用invokeinterface指令。否则用invokevirtual。

方法的调用需要1+N个操作数,其中第一个操作数是uint16索引,在字节码中紧跟在指令操作码的后面。通过这个索引,可以从当前类运行池常量中找到一个方法符号引用,解析这个符号就可以得到一个方法。注意,这个方法并不一定就是最终要调用的那个方法,所以可能还需要一个查找过程才能找到最终要调用的方法。剩下N个操作数是要传递给被调用方法的参数,从操作数栈中弹出。

如果要执行的是java方法,下一步给这个方法创建一个新的帧,并把它推导Java虚拟机栈顶。

方法的最后一条指令是某个返回指令,这个指令负责把方法的返回值推入前一帧的操作数栈顶,然后把当前帧从java虚拟机栈中弹出。

7.2 解析方法符号引用

对于非接口方法引用。如果类D想要通过方法符号引用访问类C的某个方法,先要解析符号引用得到类C。如果C是接口,抛异常。否则根据方法名和描述符查找方法,先从C的继承层次中找,如果找不到,就去C的接口找,如果还是找不到抛异常。否则检查类D是否有权限访问该方法,如果没有,抛异常。

对于接口方法引用。与上面大同小异。

7.3 方法调用和参数传递

在定位到需要调用的方法之后,Java虚拟机要给这个方法创建一个新的帧并把它推入Java虚拟机栈顶,然后传递参数。

对于参数传递。首先,要确定方法的参数在局部变量表中占多少位置。注意,这个数量并不一定等于从Java代码总看到的参数个数,原因①long和double类型要占用两个位置②对于实例方法,java编译器会在参数列表前面添加一个参数,这个隐藏参数就是this引用。

7.4 返回指令

return指令只需要把当前帧冲java虚拟机栈中弹出即可。

其他返回指令需要从把方法的返回值推入前一帧的操作数栈顶,然后把当前帧从java虚拟机栈中弹出。

7.5 方法调用指令

invokestataic指令。假定解析符号引用后得到方法M。M必须是静态方法,否则抛出异常。(M不能是类初始化方法!类初始化方法只能由Java虚拟机调用,不能使用invokestatic调用!这一规则有class文件验证器保证)如果声明M的类还没有被初始化,则要先初始化该类。

invokespecial指令。①先拿到当前类、当前常量池、方法符号引用,然后解析符号引用、拿到解析后的类和方法。②假定从方法符号引用中解析出来的类是C,方法是M。如果M是构造函数,则声明M的类必须是C,否则抛出异常。如果是静态方法,抛异常。③操作数栈弹出this引用,如果是null,抛异常(NullPointerException)。注意,在传递参数之前,不能破坏操作数栈的状态。④确保protected方法只能被声明该方法的类或子类调用,如果违反则抛异常。⑤如果调用超类中的函数但不是构造函数,且当前类的ACC_SPUER标志被设置,需要一个额外查找过程寻找最终要调用的方法,否则前面从方法符号引用解析出来的方法就是要调用的方法。⑥如果查找失败,或者找到的方法是抽象的,抛异常。如果找到就调用。

invokespecial指令。①②③与invokespecial大同小异。④从对象的类中查找真正要调用的方法,如果找到的是一个抽象方法则抛异常,否则正常

invokeinterface指令。该指令操作码后面跟着4字节,而上述三条指令都是2字节。前两字节含义和其他指令相同,是uint16类型的运行时常量池索引。第三字节是给方法传递参数需要的slot数。第四字节必须是0(给oracle的某些jvm使用)。①先从运行时常量池中拿到解析接口方法符号引用,如果解析方法是静态方法或者私有方法抛异常。②从操作数栈弹出this引用,如果是null抛异常;如果引用所指对象的类没有实现解析出来的接口,抛异常。③查找最终调用的方法。如果找不到,或者找到的方法是抽象的,抛异常;如果找到的方法不是public,抛异常。否则正常。

7.6 改进解释器

改进loop(循环):

每次循环开始,先拿到当前帧,然后根据pc从当前方法中解码一条指令。指令执行完毕之后,判断Java虚拟机栈中是否还有帧。如果没有则退出循环;否则继续。

7.7 类初始化

类初始化就是执行类的初始化方法(<clinit>)。类的初始化方法发生在:

①执行new指令创建类实例,但类还有没被初始化。

②执行putstatic、getstatic指令存取类的静态变量,但声明该字段的类还有没被初始化。

③执行invokestatic调用类的静态方法,但是声明该方法的类还有没被初始化。

④当初始化一个类时,它的超类还没有初始化,要先初始化超类。

⑤执行某些反射操作时。

因此,我们需要修改这四条指令的逻辑。先判断类的初始化是否已经开始,如果还没有,则需要调用类的初始化方法,并终止指令执行。但是由于PC已经执行了指向下一条指令,所以要修改PC的值使其重新指向当前指令。

猜你喜欢

转载自blog.csdn.net/monkeydcoding/article/details/81315550