这里也是扩展篇之动态代理里面的内容,也是单独把它拿出来了,详细的可以去看扩展篇之动态代理。
这里就涉及到java的多态,多态是什么呢?
允许不同类的对象对同一消息做出响应,即根据发送对象的不同而采用多种不同的行为方式。
实现多态的技术称为动态绑定,指的是在运行期间判断对象所引用对象的实际类型,根据实际类型调用其相应的方法。另外一种就是在编译期进行绑定,也就是我们所说的静态绑定。
JVM实现晚期绑定的机制是基于virtual table,即虚方法表。JVM通过虚方法表在运行期动态的确定所调用目标类的目标方法。
在JVM加载Java类的过程中,JVM会动态的解析Java类的方法及其对父类方法的重写,进而构建出一个vtable.
java类在运行期进行动态绑定的方法,一定会被声明为public或者protected,并且没有static和final修饰.
原因是,如果java方法已经被static修饰,就根本不会参与到整个java的继承体系中。即动态绑定,其实可以理解为Java类实例与java方法搭配。静态方法根本不需要经过类实例。
java方法被private修饰,外部无法调用,因此也不会参与到继承体系。
方法被final修饰,子类无法重写,自然也不会出现多态。
若子类重写了父类的方法,则JVM会更新父类虚方法表中指向父类被重写方法的指针,让其指向子类中该方法的内存地址。如果子类中方法不是对父类方法的重写,JVM会向子类的虚方法表中插入一个新的指针元素,让其指向该方法的内存位置。
Java的字节码指令中方法的调用实现分为4种指令
Invokevirtual,包含了virtual dispatch(虚方法分发)机制。
Invokespecial,调用private和构造方法,绕过了虚方法分发
Invokeinterface,实现与invokevirtual类似。
Invokestatic,调用静态方法。
我们着重写下invokevirtual
虚拟机在执行invokevirtual指令的过程中,最终会读取被调用的类的虚方法表,并据此决定真是的目标调用方法。
我们可以验证一下
public class Animal {
public void say(){
System.out.println("I am animal");
}
public static void main(String[] args) {
Animal animal = new Dog();
run(animal);
animal = new Animal();
run(animal);
}
public static void run(Animal animal){
animal.say();
}
static class Dog extends Animal{
@Override
public void say() {
System.out.println("I am a dog");
}
}
}
输出结果为
I am a dog
I am animal
我们查看Animal.class的字节码可以看到
java多态验证.pngpublic static void run(com.yjm.Animal);
descriptor: (Lcom/yjm/Animal;)V
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=1, locals=1, args_size=1
0: aload_0
1: invokevirtual #10 // Method say:()V
4: return
LineNumberTable:
line 19: 0
line 20: 4
LocalVariableTable:
Start Length Slot Name Signature
0 5 0 animal Lcom/yjm/Animal;
为了便于理解字节码,可以看之前自己画的一个图,当然是在不考虑栈顶缓存的情况下,栈顶缓存主要是为了执行效率优化,比较复杂,所以画的是不考虑栈顶缓存的情况。
执行引擎(不考虑栈顶缓存).png我们继续我们的animal字节码指令
aload_0表示从第0个slot位置加载java引用对象,由于Animal.run(Animal)方法是一个static静态方法,其入参并没有隐式的this指针,所以slot中的第一个局部变量就是Animal.run(Animal)的第一个入参的animal引用对象。接着就是第二步执行invokevirtual指令,invokevirtual指令后面的操作数是常量池的索引值,所代表的字符串是Method say:()V,代表运行期调用的是void say()。
在运行期,jvm将首先确定被调用的方法所属的java类的实例对象,jvm会读取被调用方法的堆栈,并获取堆栈中的局部变量表中的第0个slot位置的数据,该数据指向的是被调用方法所属的java类实例。
获取到类实例之后,便能通过对象获取到其对应的虚方法分发表。
当animal引用变量指向new Dog()对象的实例时,jvm会遍历Dog类锁对应的虚方法表,并搜搜其中名称为"say",签名为"void ()" 的方法,Dog类中存在该方法,jvm最终执行的就是Dog类的say()方法。同理,当animal 引用变量指向new Animal() 对象实例时,jvm最终执行的就是Animal类中的say()方法。
网友评论