接上一节记录,方法调用不等于方法的执行,方法调用的唯一任务是确定被调用方法。暂时还没有涉及具体的运行过程。一切方法调用在Class文件存储的都只是符号引用,并不是实际的内存布局中的入口地址(直接引用),但是在类加载的解析阶段,会讲一部分符号引用转为直接引用,前提必须是方法是可确定,并且运行期间不可改变。
字节码指令中指出了哪些是可以在类加载就转为直接引用的,如下:
上图中的invokestatic 和 invokespecial指令调用的方法,都可以将符号引用转为直接引用,使用静态方法,私有方法,实例构造器,父类方法,final方法满足要求。这些都称之为非虚方法,举例如下:
字节码文件分派调用自然和解析调用不同,分为静态和动态
静态分派调用(在编译时期)
这里强调的是虚拟机在重载时是通过参数的静态类型而不是实际类型来作为判定依据,如下:
结果两个都输出hello ,guy!
动态分派(发生在运行时期)
运行时候根据类型确定方法执行版本的分派过程称为动态分派,常见的就是多态的例子
上图是我使用多态的一个例子,可以看到动态分派使用的是invokevirtual指令。运行时会有相关栈帧,而栈帧中又包含了局部变量表,操作数栈,动态链接等等。个人简单理解动态分派就是操作数栈的出入顺序加上常量池中相关信息比如方法描述符等实现的。
动态分派还可以根据方法的接收者和方法参数分为单分派和多分派
运行结果 father choose 360 son choose qq
动态分派属于单分派类型,静态分派属于多分派类型,因为静态分派需要确定两点:
1 静态类型是father还是son
2 方法参数是QQ还是360
实际上这里的参数匹配是通过参数的自动转型来找合适的方法,如果恰好有多个合适的方法,那么第一个方法(即范围大的)相对不合适。
方法表
由于动态分派非常频繁,并且在动态分派的方法版本选择过程需要运行时查找类的目标方法,虚拟机在方法区为类在方法区建立了一个方法表。虚方法表中存放着各个方法的实际入口地址。如果某个方法在子类中没有被重写,那子类的虚方法表里面的地址入口和父类相同方法的地址入口是一致的,都指向父类的实现入口。如果子类中重写了这个方法,子类方法表中的地址将会替换为指向子类实现版本的入口。
方法表一般在类加载的连接阶段进行初始化,准备了类的变量初始值之后,虚拟机会把该类的方法表也初始化完毕。
网友评论