在《Java核心技术卷一》一书中,对方法调用有着比较明确的阐述。书中写道,假设要调用x.f(args),隐式参数x声明为类C的一个对象,调用过程的详细描述:
- 编译器查看对象的声明类型和方法名,编译器一一列举所有C类中名为f的方法以及其超类中访问属性为public且名为f的方法
- 编译器查看调用方法提供的参数类型,然后找到最恰当的一个f方法,例如调用x.f(“Hello”),编译器将调用f(String)
- private, static, final方法编译器都可以准确地知道应该调用哪个方法,称之为静态绑定;其他成员方法依赖于隐式参数的实际变量,我们称之为动态绑定。
- 程序运行时,虚拟机一定调用与x所引用对象实际类型最合适的那个类方法。、
总结起来,前三步都是编译器行为,其中前两步是重载解析。编译器并不知道一个变量属于哪个真实类,它只能读取“字面值”(因为编译过程没有内存参与,更不会创建对象),因此我们发现,它是在x的声明类C中寻找适合的函数。对于private, static, final方法,编译器可以准确地知道应该调用哪个方法,我们称之为静态绑定,然而对于普通成员方法,编译器并不知道具体调用哪个类的哪个方法,因此生成一条动态绑定指令。换句话说,当我们使用javac命令编译完字节码文件之后,这三步已经完成了。 第四步是虚拟机行为,虚拟机知道x的真实类,例如这里x的真实类是D,如果D有f(String)函数,那就直接调用它。
以上是从Java语言层面解释方法调用以及多态的过程,那么其在编译或者运行过程中,具体是怎么样的呢?看下面
public class Demo {
private void test1() { }
private final void test2() { }
public void test3() { }
public static void test4() { }
public static void main(String[] args) {
Demo d = new Demo();
d.test1();
d.test2();
d.test3();
d.test4();
}
}
上面代码字节码反编译后的结果:
public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
flags: (0x0009) ACC_PUBLIC, ACC_STATIC
Code:
stack=2, locals=2, args_size=1
0: new #2 // class Demo
3: dup
4: invokespecial #3 // Method "<init>":()V
7: astore_1
8: aload_1
9: invokespecial #4 // Method test1:()V
12: aload_1
13: invokespecial #5 // Method test2:()V
16: aload_1
17: invokevirtual #6 // Method test3:()V
20: aload_1
21: pop
22: invokestatic #7 // Method test4:()V
25: return
根据《Java核心技术》中的概念,在编译成字节码之后,已经完成了前三步,那么静态绑定和动态绑定是如何体现出来的呢? new是创建对象,然后将对象的引用放入操作栈,同时复制栈顶元素并且入栈,然后调用了Demo类的初始化方法,并把对象引用弹栈,此时操作数栈中还剩下一个对象引用,然后将其复制到局部变量表1的位置,至此,Demo对象创建完毕。 在java字节码中,用invokevirtual调用普通成员方法,用invokespecial调用构造方法、私有方法和最终(final)方法,用invokestatic调用类方法。因此,所谓的动态绑定就是用invokevirtual标明的。
比较有趣的点是,在Java语言中,我们可以使用一个对象调用其类方法,也就是d.test4(), 我们观察字节码中发生了什么。字节码的第20行-22行对应着该过程,我们发现,虚拟机会先把局部变量表1位置的元素加载到栈中,然后又退栈,接着调用了test4(),说明调用test4()是不需要类对象的。
我们还会好奇,invokevirtual到底是如何实现多态的呢?
补充学习:常量池各表的关系https://blog.csdn.net/huangrunqing/article/details/51996424
当执行invokevirtual指令时,
- 先通过操作数栈中的对象引用找到对象
- 分析对象头([[Java内存区域]]中的对象创建过程),找到对象的实际Class
- Class结构中有vtable,它在类加载的链接阶段就已经根据方法的重写规则生成好了
- 查表得到方法的具体地址
- 执行方法的字节码