JAVA方法调用属于虚拟机字节码执行引擎的一部分,执行引擎,可以简单的理解为它用来接收输入的Class文件,按照字节码进行处理程序,然后输出执行结果。
我们在如何找个对象中已经讲述了关于方法调用的指令,那么今天我们就看一下方法调用的时候虚拟机引擎会做哪些事。
1. 方法调用
由于Java语言的多态性质(重写、重载),因此我们的方法调用需要确认需要调用哪个方法。这里不牵涉方法的执行。
2. 如何确定方法
如何确定方法大致有两种方式:
解析
分派
解析是发生在编译阶段即可确定方法的版本,然后在类加载的连接阶段中的解析步骤将符号引用替换为直接引用。
分派既有可能在编译阶段确定,也有可能在运行期确定。
3. 解析
解析是在编译期就可确定方法的版本,然后在类加载的连接阶段中的解析步骤中将一些符号引用转换为直接引用,那么符合这些条件的符号引用涉及的方法有以下特点:
编译器可知,运行期不变,
符合上述特点的方法主要有:静态方法(invokestatic)、私有方法(invokespecial)、实例构造器(invokespecial)、父类方法(invokespecial),这些方法均可在类加载的解析阶段便可将符号引用替换为直接引用,这些方法统称为非虚方法(final方法也是非虚方法)。
4. 分派
分派有静态分派、动态分派、静态多分派、动态单分派。
4.1 静态分派
静态分派通常用在Java的重载上。
Car car = new ChaoPao();
上面是一行简单的Java代码,Car是一个父类,代表了汽车的基础类型,而ChaoPao则是汽车中的一个种类。在这里我们说明一个概念:
car变量的静态类型为Car,动态类型为ChaoPao。
所有基于静态类型来确定方法调用版本的方式都属于静态分派。静态分派发生在编译阶段,因此并不是由虚拟机执行的,编译器在确定方法版本的时候会选取于一个最合适的版本。
package jvm;
import java.io.Serializable;
/**
* @author sh
*/
public class ClassTest{
public void test(char i){
System.out.println("i am char");
}
public void test(int i){
System.out.println("i am int");
}
public void test(long i){
System.out.println("i am long");
}
public void test(Character i){
System.out.println("i am Character Object");
}
public void test(Serializable i){
System.out.println("i am Serializable");
}
public void test(Object i){
System.out.println("i am Object");
}
public void test(char... i){
System.out.println("i am variable length i");
}
public static void main(String[] args){
ClassTest test = new ClassTest();
test.test('a');
}
}
上面这段代码比较极端,在实际开发中几乎不会见到这样的代码,这里我们只是演示了何为叫静态分派会选择一个较为合适的版本,上述代码的方法的顺序便是我们main方法中静态分派选择的顺序,越靠上的越合适,优先级越高。变长参数的优先级最低。
4.2 动态分派
动态分派主要用在方法的重写。
package jvm;
/**
* @author sh
*/
public class ClassTest{
public static abstract class Human{
abstract void print();
}
public static class WoMan extends Human{
@Override
void print(){
System.out.println("I am Woman");
}
}
public static class Man extends Human{
@Override
void print(){
System.out.println("I am Man");
}
}
public static void main(String[] args){
Human woman = new WoMan();
Human man = new Man();
woman.print();
man.print();
man = new WoMan();
man.print();
}
}
上面的结果就不说了,熟悉Java的同学一眼便能知道结果。下面我们主要分析动态分派和重写有什么关系。
我们主要看一下12:invokevirtual,这一行字节码实际就是下面这行代码的字节码指令:
woman.print();
之多以能产生多态(动态分派),需要从invokevirtual的运行过程来进行分析,如下:
找到操作数栈顶的第一个元素所指向的对象的实际类型,记做C
在类型C中在找到与常量中的描述符和简单名称都相同的方法,如果有,接着进行方法权限的校验,校验通过以后返回此方法的直接引用,查找结束;如果校验不通过,抛出java.lang.IllegalAccessError异常
否则,按照继承关系从下往上依次对C的父类进行第2步的查找过程,如果一直没有找到,则抛出java.lang.AbstractMethodError异常
4.3 静态多分派、动态单分派
静态多分派和动态单分派并不是两种新的分派类型,只不过是对静态分派和动态分派再加了一层修饰词。
单、多的定义是通过影响方法确定的元素数量来区分,如果是多个便是多分派,如果是单个便是单分派。
public class ClassTest{
public static abstract class Human{
abstract void print(Human human);
abstract void print(Man man);
abstract void print(WoMan woMan);
}
public static class WoMan extends Human{
@Override
void print(Human human){
System.out.println("WoMan say Human");
}
@Override
void print(Man man){
System.out.println("Woman say Man");
}
@Override
void print(WoMan woMan){
System.out.println("Woman say WoMan");
}
}
public static class Man extends Human{
@Override
void print(Human human){
System.out.println("Man say Human");
}
@Override
void print(Man man){
System.out.println("Man say Man");
}
@Override
void print(WoMan woMan){
System.out.println("Man say WoMan");
}
}
public static void main(String[] args){
Human woman = new WoMan();
Human man = new Man();
Human params = new WoMan();
woman.print(params);
man.print(params);
}
}
首先我们看编译过程,也就是静态分派的过程,woman.print(params),这是选择目标方法需要两个依据:
woman的静态类型是Man、WoMan或者是Human,这里是HuMan
第二个是方法参数,是Man、WoMan或者是Human,这里是HuMan
因此静态分派受两个元素的影响,因此Java中的静态分派是静态多分派。
通过上图可以看出invokevirtual的符号引用和我们的分析一致。
下面我们再看一下运行期,在虚拟机运行的时候他需要获取woman的具体类型,这个可以在回看一下动态分派的过程,但是此时的方法签名已经固定为print:(Ljvm/ClassTest$Human;)V,在这里方法中的参数就是HuMan,不再关心是Man还是WoMan,也就是说虚拟机的动态分派只受变量的具体类型影响,不受方法参数的影响(因为在静态分派中已经确定),因此Java中的动态分派是动态单分派。
本期虚拟机层面的Java方法调用介绍到这,我们下期再见!!!
我是shysh95,希望可以和你专注技术的路上并肩作战,长按识别或者扫码关注微信公众号,更多精彩文章!!!