代码的执行其实本质上是方法的执行,站在JVM的角度归根到底还是字节码的执行。
main函数是JVM指令执行的起点,JVM会创建main线程来执行main函数,以触发JVM一系列指令的执行,真正地把JVM跑起来。这个过程就是方法调用的过程。
在一些重型框架中,我们有时候看不到main在哪里,那是因为被框架封装了,但是在各种微服务中,我们都能轻松找到main方法。
接下来,我们深入了解方法在JVM中的调用。
1.方法调用的字节码指令
- invokestatic 用来调用静态方法;
- invokespecial 用于调用私有实例方法、构造器及super关键字等;
- invokevirtual 用于调用非私有实例方法,比如public和protected,大多数方法调用属于这一种;
- invokeinterface 和上面这条指令类似,不过作用于接口类;
- invokedynamic 用于调用动态方法。
2.非虚方法
如果方法在编译期就确定了具体的调用版本,这个版本在运行时是不可变的,这样的方法称为非虚方法。
只要能被 invokestatic 和 invokespecial 指令调用的方法,都可以在解析阶段中确定唯一的调用版本,Java 语言里符合这个条件的方法共有静态方法、私有方法、实例构造器、父类方法4种,再加上被 final 修饰的方法(尽管它使用 invokevirtual 指令调用),这5种方法调用会在类加载的时候就可以把符号引用解析为该方法的直接引用。不需要在运行时再去完成。
- invokestatic用来调用静态方法
这个方法调用在编译期间就明确以常量池项的形式固化在字节码指令的参数之中了。
- invokespecial用于调用私有实例方法、构造器及super关键字等
这个方法调用在编译期间同样明确以常量池项的形式固化在字节码指令的参数之中了。
3.虚方法
与非虚方法相反,不是虚方法的方法就是虚方法。主要包括以下字节码中的两类
- invokevirtual用于调用非私有实例方法,比如public和protected,大多数方法调用属于这一种(排除掉被final修饰的方法)
- invokeinterface和上面这条指令类似,不过作用于接口类
为什么叫做虚方法呢?就是方法在运行时是可变的。
很多时候,JVM需要根据调用者的动态类型,来确定调用的目标方法,这就是动态绑定的过程;相对比,invokestatic指令加上invokespecial指令,就属于静态绑定过程。
因为invokeinterface指令跟invokevirtual类似,只是作用与接口,所以我们只要熟悉invokevirtual即可。
3.1分派
要了解虚方法我们必须了解以下基础:
Java 是一门面向对象的程序语言,因为Java 具备面向对象的3个基本特征:继承、封装和多态。
分派调用过程将会揭示多态性特征的一些最基本的体现,如“重载”和“重写”在 Java 虚拟机之中是如何实现的
3.1.1 静态分派
多见于方法的重载
重载:一个类中允许同时存在一个以上的同名方法,这些方法的参数个数或者类型不同
“Human”称为变量的静态类型(Static Type),或者叫外观类型(Apparent Type),后面的“Man”和“Woman”则称为变量的实际类型(Actual Type)。
静态类型和实际类型在程序中都可以发生一些变化,区别是静态类型的变化仅仅在使用时发生,变量本身的静态类型不会被改变,并且最终的静态类型是在编译期可知的;而实际类型变化的结果在运行期才可确定,编译器在编译程序的时候并不知道一个对象的实际类型是什么。
代码中定义了两个静态类型相同但实际类型不同的变量,但虚拟机(准确地说是编译器)在重载时是通过参数的静态类型而不是实际类型作为判定依据的。并且静态类型是编译期可知的,因此,在编译阶段,Javac编译器会根据参数的静态类型决定使用哪个重载版本,所以选择了sayHello(Human) 作为调用目标。所有依赖静态类型来定位方法执行版本的分派动作称为静态分派。
静态分派的典型应用是方法重载。静态分派发生在编译阶段,因此确定静态分派的动作实际上不是由虚拟机来执行的。 所以代码运行结果如下:
Hello, guy
Hello, guy
Process finished with exit code 0
通过字节码分析你会发现其实入参类型其实是Human
总结:方法会根据你送入的参数有不同的表现形式,这个就是分派。但是静态分派只调用它最原始的外观类型。
不过我们可以通过强转来修改静态类型
3.1.2 动态分派
多见于方法的重写。
重写:在子类中将父类的成员方法的名称保留,重新编写成员方法的实现内容,更改方法的访问权限,修改返回类型的为父类返回类型的子类。
重写也是使用invokevirtual指令,只是这个时候具备多态性。
invokevirtual指令有多态查找机制,该指令运行时,解析过程如下:
- 找到操作数栈的第一个元素所指向的对象实际类型,记做c;
- 如果在类型c中找到与常量中的描述符和简单名称都相符的方法,则进行访问权限校验,如果通过则返回这个方法直接引用,查找过程结束,不通过则返回java.lang.IllegalAccessError,否则,按照继承关系从下往上依次对c的各个父类进行第二步的搜索和验证过程;
- 如果始终没找到合适的方法,则抛出java.lang.AbstractMethodError异常,这就是Java语言中方法重写的本质 、
这个时候我如果结合之前课程中讲过虚拟机栈中栈中的内容,我就知道动态链接是干嘛的。
invokevirtual可以知道方法call()的符号引用转换是在运行时期完成的,在方法调用的时候。部分符号引用在运行期间转化为直接引用,这种转化就是动态链接。
方法表:
动态分派会执行非常频繁的动作,JVM运行时会频繁的、反复的去搜索元数据,所以JVM使用了一种优化手段,这个就是在方法区中建立一个虚方法表。
使用虚方法表索引来替代元数据查找以提高性能。
3.2 接口调用
invokeinterface和invokevirtual 指令类似,不过作用于接口类;
3.3 invokedynamic
这个指令通常在Lambda语法中出现,主要用于一些动态的调用场景。迟点再总结Lambda。