1、静态引用与动态引用
在JVM中,将符号的引用转换为调用方法的直接引用与方法的绑定机制有关
-
静态引用(静态链接)
当字节码文件被装入JVM内部时,如果被调用的方法在编译期确定,且运行期保持不变,这种情况下将调用方法的符号引用转换为直接引用的过程称为静态引用,或静态链接; -
动态引用(动态链接)
如果被调用的方法在编译期不确定,在运行期将调用方法的符号引用转换为直接引用的工程称为动态引用,或动态链接。
如下代码所示,有一个基类Persion,子类Man继承Persion
public class Persion {
public Persion(){
System.out.println("this is persion class");
}
public void show(){
System.out.println("I'm a persion");
}
}
public class Man extends Persion{
public Man(){
super();
System.out.println("this is man class");
}
@java.lang.Override
public void show() {
System.out.println("I'm a man");
}
}
新建测试类
public class MethodDemo {
public static void main(String[] args) {
test1(new Man());
}
public static void test1(Persion persion){
persion.show();
}
}
输出结果:
this is persion class
this is man class
I'm a man
上述示例中,test1方法中调用persion.show()时,由于编译期不确定,在运行时通过传入的persion的实际类型才可以确定,属于动态链接,通过字节码可以窥探,invokevirtual 表示调用虚方法,非真实的,等到运行期时才可以确定调用的真实方法。
public static void test1(com.lzj.runtime.method.Persion);
descriptor: (Lcom/lzj/runtime/method/Persion;)V
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=1, locals=1, args_size=1
0: aload_0
1: invokevirtual #2 // Method com/lzj/runtime/method/Persion.show:()V
4: return
再比如Man的初始化方法中,调用了super(),在编译期可以明确就是调用的是父类Persion中的初始化方法,属于静态链接,通过字节码窥探,可见调用super()时用的invokespecial,表示编译期就可以找到具体的方法。
public com.lzj.runtime.method.Man();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=2, locals=1, args_size=1
0: aload_0
1: invokespecial #1 // Method com/lzj/runtime/method/Persion."<init>":()V
4: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;
7: ldc #3 // String this is man class
9: invokevirtual #4 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
12: return
静态链接属于早期绑定,绑定就是一个字段、方法或者类的符号引用被替换为直接引用的过程,静态链接在编译期就可以绑定到具体的字段、方法或者类符号的真实引用;而动态链接属于晚期绑定,在运行期才可以确定具体字段、方法或者类符号的真实引用。
2、虚方法与非虚方法
如果在编译期间就确定调某方法的具体版本,并且在运行期间保持不变,这样的方法称为非虚方法。
静态方法、私有方法、final方法、实例构造器、父类方法都是非虚方法,其他方法为虚方法。
可以通过JVM调用方法的执行来判断是否虚方法,在JVM中调用方法的指令主要由下面4中方式组成:
- invokestatic: 调用静态方法;
- invokespecial: 调用构造器方法、私有方法以及父类方法;
- invokevirtual: 调用虚方法;
- invokeinterface: 调用接口方法;
- invokedynamic: 动态解析调用的方法。
其中invokestatic和invokespecial在编译阶段即可确定调用方法的版本,这些方法统称为非虚方法;
invokevirtual、invokeinterface和invokedynamic在运行阶段才可确定调用方法的具体版本,为虚方法,invokevirtual中除了final类型方法特殊,final类型修饰的方法在编译期即可 确定调用的具体版本,为非虚方法。
例如如下代码,Map继承Persion,Dog实现Animal
public class MethodDemo {
public static void main(String[] args) {
MethodDemo demo = new MethodDemo();
demo.test2();
test1(new Man());
Animal animal = new Dog();
animal.eat();
}
public static void test1(Persion persion){
persion.show();
}
public final void test2(){
System.out.println("I'm test2");
}
}
此段代码经过编译成JVM字节码如下,注释已标记在调用方法之上。
public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=2, locals=3, args_size=1
0: new #2 // class com/lzj/runtime/method/MethodDemo
3: dup
//invokespecial #3表示执行了MethodDemo的构造器方法,非虚方法
4: invokespecial #3 // Method "<init>":()V
7: astore_1
8: aload_1
//invokevirtual #4 表示调用test2()方法,由于test2方法是final类型的,所以为invokevirtual,实际test2方法为非虚方法
9: invokevirtual #4 // Method test2:()V
12: new #5 // class com/lzj/runtime/method/Man
15: dup
//调用Man的构造器方法,非虚方法
16: invokespecial #6 // Method com/lzj/runtime/method/Man."<init>":()V
//调用静态方法,非虚方法
19: invokestatic #7 // Method test1:(Lcom/lzj/runtime/method/Persion;)V
22: new #8 // class com/lzj/runtime/method/Dog
25: dup
//调用Dog的构造器,非虚方法
26: invokespecial #9 // Method com/lzj/runtime/method/Dog."<init>":()V
29: astore_2
30: aload_2
//调用接口中方法,虚方法
31: invokeinterface #10, 1 // InterfaceMethod com/lzj/runtime/method/Animal.eat:()V
36: return
invokedynamic案例
动态语言与静态语言的区别在于类型检查在编译期还是运行期,在运行期为静态语言,在编译期的为动态语言。
比如python中 moneny = 18.9,在运行期时判定moneny为float类型,而对于java,float moneny = 18.9,在编译期就确定了moneny的float类型,为静态语言。但JVM中为了支持动态语言的特性,引入了invokedynamic指令,例如如下代码
public class MethodDemo2 {
public static void main(String[] args) {
Consumer consumer = x ->
System.out.println("I hava " + x + "$");
consumer.accept(5);
}
}
编译成字节码指令为:
//可见在定义Consumer时用的invokedynamic指令,也就是说java中函数式接口是通过invokedynamic方式执行的,通过这种方式支持动态语言的特性
0 invokedynamic #2 <accept, BootstrapMethods #0>
5 astore_1
6 aload_1
7 iconst_5
8 invokestatic #3 <java/lang/Integer.valueOf>
11 invokeinterface #4 <java/util/function/Consumer.accept> count 2
16 return
3、虚方法表使用
重写方法调用原理:
比如一个基类Animal,子类Dog继承Animal,当通过animal对象调用对象中方法时,是调用的Animal中的方法呢,还是调用的Dog中的方法呢?很明显,都知道根据java的多态性,调用的是Dog中的方法,其原理是什么?是如何做到呢?
以Animal animal = new Dog()为例,调用animal.eat()
首先找到操作数栈最顶端的元素对象animal,然后JVM会通过invokedynamic指令会找到animal的实际类型即Dog,如果在Dog中可以找到eat方法,则校验是否有访问Dog中eat方法权限,如果有权限,invokedynamic后的字符应用会指向常量池中Dog中的eat方法地址;如果没有权限,则抛异常java.lang.IllegaAccessError异常(试图访问没有访问权限的属性或方法,编译期异常;如果该错误发生在运行期,说明发生了不兼容的改变,比如maven中引用了多个版本,版本中属性或方法不一致)。
如果没找到Dog中有eat方法,则重复上述步骤,从Dog的父类Animal中找eat方法,如果找到如上,也是校验权限,指向常量池中方法地址,如果最终没有找到合适方法,则抛出java.lang.AbstractMethodError异常(比如一个子类implements 了一个interface,但未重写其中的方法,在调用该方法时发生此类错误)
**虚方发表** 对于上述原理,如果有很多层级的继承或实现,那么要查询调用的方法,可能要查询多次才能从最上层的父类查找到真实的调用方法,这种方式效率会非常低下。为了提高性能,JVM在类的方法区建立一个虚方法表(virtual method table)来应对该为。 每个类中都有一个虚方法表,表中存放着各个方法的实际入口。 虚方法表在类加载的链接阶段中的解析阶段创建并初始化,类的变量初始值在准备阶段初始完毕,之后,JVM在解析阶段初始化虚方法表。
例如,如下案例,Father类继承Object,并只在Father类中定义了hardChoice(QQ)和hardChoice(_360)方法,Son类继承自Father类,只重写了hardChoice(QQ)和hardChoice(_360)方法。
当对Son的对象调用hardChoice(QQ)或hardChoice(_360)方法时,在Son类的虚方法表中查找该两个方法在Son类中,然后直接调用;当调用clone、equals等方法时,虚方法表显示直接去Object类中调用,避免了一层层向上父类查找时间。
当对Father对象调用hardChoice(QQ)或hardChoice(_360)方法时,在Father类虚方法表中查找该两个方法在Father类中,直接调用;当调用clone、equals等方法时,虚方法表显示直接去Object类中调用。