介绍
每一个栈帧内部都包含一个指向运行时常量池中该栈帧所属方法的引用,包含这个引用就是为了支持当前方法的代码能够实现动态链接。
在java源文件被编译到字节码文件中时,所有变量和方法引用都作为符号引用保存在class文件的常量池里。比如:描述一个方法调用了另外方法时,就是通过常量池中指向方法的符号引用来表示的。
动态链接就是把符号引用转为调用方法的直接引用的过程
代码说明
在console中,调用javap -v xx.class解析任一字节码文件,我们可以得到一个常量池:
我们查看任一方法(比如main()方法),如下:
发现每个使用方法的指令后面都会跟着#,这就指向运行时常量池中的对应方法。这些引用都称为符号引用。
方法的调用
静态链接:
如果一个class文件被装载进JVM内部,被调用的目标方法在编译期可知,且运行期间不变时,将符号引用转为直接引用的过程称为静态链接
动态链接:
编译期无法确定,只能在程序运行期间将调用方法的符号引用转为直接引用。
绑定是一个字段、方法或者类在符号引用被替换为直接引用的过程,仅发生一次。
早期绑定:
编译器可知,运行期不变。方法可以与类型绑定,由于明确了被调用的目标方法,因此可以使用静态链接的方式将符号引用转为直接引用
晚期绑定:
编译期无法确定,只能在程序运行期间根据实际类型绑定方法。
java中任何一个普通方法都具备虚函数的特征(C++),标志为final的方法才不具备多态的特征(不能被重写)
虚方法与非虚方法
非虚方法:在编译时就确定了具体版本
静态static、私有private、final、实例构造器、父类方法(super.xx())都是非虚方法。
其余方法都是虚方法
多态性的使用前提:类的继承关系(super.xx());方法的重写(静态、私有、final方法不能被重写)。
JVM普通调用指令:
- invokestatic: 调用静态方法
- invokespecial: 调用<init>、私有及父类方法
- invokevirtual: 调用所有虚方法(final也是这个指令)
- invokeinterface:调用接口方法
动态调用:
invokedynamic: 动态解析
前面四条指令是固定的,无法人为干预。
而动态调用指令由用户确定方法版本, 其中前两天指令称为非虚方法,其余(除final外)称为虚方法。
动态调用指令直到Java7才增加。
但直到java8的lambda的出现,这个指令才有了直接生成的方式。
动态类型语言&静态类型语言
区别:对于类型的检查在编译期还是在运行期,前者是静态,后者是动态。
静态类型语言是判断变量自身的类型信息;
动态类型语言是判断变量值的类型信息,变量没有类型信息,变量值才有类型信息。
比如python赋值不用声明变量类型,而是根据值的类型来判断变量的类型,所以是动态类型语言。
方法重写的本质
- 找到操作数栈顶第一个元素执行对象的实际类型,记作C
- 如果在C中找到符合signature+返回值的方法,则进行访问权限校验,通过则返回引用;否则返回IllegalAccessError(一般出现在编译期)
- 否则,按继承关系从子类到父类进行第二步。
- 如果始终没有,则抛出AbstractMethodError(说明没有实现接口方法)
虚方法表
为了防止每次都要寻找合适目标,提高性能,采用在类的方法区建立一个虚方法表。
每个类都有一个虚方法表,表中存放着各个方法的实际入口。
虚方法表在类加载的链接阶段创建并开始初始化,类的变量初始值准备完成后,JVM会把类的方法表也初始化完毕。
方法表大致是这样的形式:
即对每一个方法指明,这个方法来自于本身还是继承于父类。