虚方法和非虚方法

  • 非虚方法:方法在编译器就确定了具体的调用版本,这个版本的运行时是不可变的(非虚方法有静态方法、私有方法、final方法、实例构造器、父类方法,其他方法为虚方法)。

  • jvm提供了以下几条方法调用指令:

    • 调用非虚方法(final修饰的方法除外):
      • (1)invokestatic:调用静态方法,解析阶段确定唯一方法版本。
      • (2)invokespecial:调用方法、私有方法、父类方法,解析阶段确定唯一方法版本。
    • 调用虚方法:
      • (3)invokevirtual:调用所有虚方法。(调用final方法用的是这个指令)
      • (4)invokeinterface:调用接口方法。
      • (5)invokedynamic:动态解析出需要调用的方法,然后执行。

前四条指令固化在虚拟机内部,方法的调用执行不可人为干预,而invokedynamic指令由用户确定方法版本,其中invokestatic和invokespecial指令调用的方法为非虚方法,其余的(final修饰的方法除外)为虚方法。

例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
public class Father {
public Father(){
System.out.println("我是父类的构造方法");
}

public static void showStatic(){
System.out.println("我是父类的静态方法");
}

public final void showFinal(){
System.out.println("我是父类的final方法");
}

public void showCommon(){
System.out.println("我是父类的普通方法");
}
}

class Son extends Father{
public Son(){
//invokespecial
super();
}

public Son(int a){
///invokespecial
this();
}

//不是重写父类的静态方法,因为静态方法不能被重写
public static void showStatic(){
System.out.println("我是子类的静态方法");
}

private void showPrivate(){
System.out.println("我是子类的私有方法");
}

public void showSonCommon(){
System.out.println("我是子类的普通方法");
}

public void show(){
//invokestatic
showStatic();

//invokestatic
super.showStatic();

//invokespecial
showPrivate();

//invokespecial
super.showCommon();

//invokevirtual,因为final方法不能被重写,所以也认为它是个非虚方法
showFinal();

//invokespecial,显式调用父类的final方法
super.showFinal();

//invokevirtual,类似上面showFinal的情况,子类可能会重写,即编译期间不确定
showCommon();

//invokespecial,显式调用父类的普通方法
super.showCommon();

//invokevirtual
showSonCommon();

ShowInterface showInterface = null;
//invokeinterface
showInterface.method();
}

public static void main(String[] args) {
Son son = new Son();
son.show();
}
}

interface ShowInterface{
void method();
}

由编译后的字节码文件可得证:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
0 invokestatic #8 <com/jvm/demo/Son.showStatic>
3 invokestatic #9 <com/jvm/demo/Father.showStatic>
6 aload_0
7 invokespecial #10 <com/jvm/demo/Son.showPrivate>
10 aload_0
11 invokespecial #11 <com/jvm/demo/Father.showCommon>
14 aload_0
15 invokevirtual #12 <com/jvm/demo/Son.showFinal>
18 aload_0
19 invokespecial #13 <com/jvm/demo/Father.showFinal>
22 aload_0
23 invokevirtual #14 <com/jvm/demo/Son.showCommon>
26 aload_0
27 invokespecial #11 <com/jvm/demo/Father.showCommon>
30 aload_0
31 invokevirtual #15 <com/jvm/demo/Son.showSonCommon>
34 aconst_null
35 astore_1
36 aload_1
37 invokeinterface #16 <com/jvm/demo/ShowInterface.method> count 1
42 return
  • 动态和静态类型语言的区别在于对类型的检查是编译期还是运行期,满足前者为静态类型,满足后者为动态类型(即静态类型语言是判断变量自身的类型信息,动态类型是判断变量值的类型信息)。

  • java语言中方法重写的本质:

    • ①找到操作数栈顶的第一个元素所执行的对象的实际类型,记做C。
    • ②如果在类型C中找到与常量中的描述符合简单名称都相符的方法,则进行访问权限校验,如果通过则返回这个方法的直接引用,查找过程结束;如果不通过,则返回java.lang.IllegalAccessError异常。
    • ③否则,按照继承关系从下往上依次对C的各个父类进行第②步的搜索和验证过程。
    • ④如果始终没有找到合适的方法,则抛出java.lang.AbstractMethodError的异常。

    为了提高性能,JVM采用在类的方法区建立一个虚方法表,使用索引表来代替查找。即每个类都有一个虚方法表,表中存放着各个方法的实际入口。

    例子:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    public class VirtualMethodTable {
    }

    interface Friendly {
    void sayHello();
    void sayGoodbye();
    }

    class Dog {
    public void sayHello() {
    }
    public String toString() {
    return "Dog";
    }
    }

    class Cat implements Friendly {
    public void eat() {
    }
    public void sayHello() {
    }
    public void sayGoodbye() {
    }
    protected void finalize() {
    }
    public String toString(){
    return "Cat";
    }
    }

    class CockerSpaniel extends Dog implements Friendly {
    public void sayHello() {
    super.sayHello();
    }
    public void sayGoodbye() {
    }
    }

    则Dog类的虚方法表:

CockerSpaniel表的虚方法表:

Cat类的虚方法表:

  • 虚方法表什么时候被创建?
    • 在类加载的链接阶段被创建并开始初始化,类的变量初始值准备完成之后,JVM会把该类的方法表也初始化完毕。