- jvm中关于方法调用的指令有invokestatic,invokespecial,invokevirtual,invokeinterface以及invokedynamic五种。
其中invokestatic与invokespecial指令在解析完成(编译完成)后就能确定所要调用的方法。
指令 | 产生情况 |
---|---|
invokestataic | 调用静态方法 |
invokespecial | 调用父类方法(super.),实例构造器以及调用私有方法 |
invokevirtual | 调用实例方法 |
invokeinterface | 调用接口方法 |
invokedynamic | 基本不使用,除了lambda |
- 变量的静态类型与实际类型。
以下代码中变量f的静态类型为F,实际类型为S.
编译完成后,一个变量的静态类型是确定的,但实际类型可能是运行时才能确定的。
class Main{
public static void main(String[] args){
F f = new S();
f.p();
x(f);
}
public static void x(F f){}
public static void x(S s){}
}
class F{
void p(){}
}
class S extends F{
void p(){
//这里调用父类方法,方法唯一所以是invokespecial
super.p();
}
}
解析与静态分派
以上代码在解析时是根据变量的静态类型来决定的,比如f.p(),实际的符号引用为F.p().x(f)方法也是根据f的静态类型来静态分派选择x(F)方法.
所以静态分派是使用调用者的静态类型以及参数的静态类型解析为F.method(P )/F.filed.
值得注意的是这个过程会寻找最合适的参数类型进行匹配,比如
x(‘c’) –> x(int p),一个静态类型为char的调用,可能解析为参数类型为int的方法(在没有x(char p)的情况下).
同理参数为子类的调用可能转换为参数类型为父类的方法(在没有子类参数方法的情况下).
由于在静态分派时既使用了调用者的静态类型以及参数的静态类型来确定一个方法,所以java是一个静态多分派的语言.动态分派.
对于incokespecial与invokestatic仅仅由静态分派就能确定方法的入口地址.但是对于invokevirtual方法,由于其调用的是对象的实例方法,所以必须动态解析.其是基于上面静态解析的结果与变量的动态类型进行实际方法查找,一般会在每个类中有相应的映射表,便于快速定位具体实现入口.java是一个动态单分派的语言.对象的创建,考虑子类继承父类,那么创建一个子类对象的时候,调用super()是不是创建了一个父类对象,并复合在子类对象中呢?答案是否定的.创建子类对象的时候,并不会创建一个父类对象,但会逐级创建父类对象的数据域到当前对象中,这块父类数据的符号引用为F.field.
一个例子如下,B继承自A,并导出内存数据如图:
import java.util.Date;
public class T{
public static void main(String[] args) throws Exception{
B b = new B();
Thread.sleep(10000000);
System.out.println(b);
}
}
class A{
private int[] xxx;
A(){
xxx = new int[1024*1024];
}
}
class B extends A{
private Date x = new Date();
private byte[] xxx = new byte[1024];
B(){
}
}
- 由于java中继承中,方法是存在动态分派的而共享的数据域不存在动态分派,所以在父类中可能引用到一个子类的重写方法(F.m() 实际调用使用S.m()),而不会使用到一个子类的重写属性(F.field只会在对象的父类内存区域寻找,S.field则优先在子类的内存区域寻找,找不到再到父类的内存区域寻找).