In my previous article Everything About Method Overloading Vs Method Overriding, I have discussed method overloading and overriding, their rules and differences.
在本文中,我们将看到JVM如何在内部处理方法的重载和覆盖,以及JVM如何识别应调用的方法。
让我们以父类为例哺乳动物和一个孩子人的我们以前的博客中的课程,以更清楚地了解它。
public class OverridingInternalExample {
private static class Mammal {
public void speak() { System.out.println("ohlllalalalalalaoaoaoa"); }
}
private static class Human extends Mammal {
@Override
public void speak() { System.out.println("Hello"); }
// Valid overload of speak
public void speak(String language) {
if (language.equals("Hindi")) System.out.println("Namaste");
else System.out.println("Hello");
}
@Override
public String toString() { return "Human Class"; }
}
// Code below contains the output and bytecode of the method calls
public static void main(String[] args) {
Mammal anyMammal = new Mammal();
anyMammal.speak(); // Output - ohlllalalalalalaoaoaoa
// 10: invokevirtual #4 // Method org/programming/mitra/exercises/OverridingInternalExample$Mammal.speak:()V
Mammal humanMammal = new Human();
humanMammal.speak(); // Output - Hello
// 23: invokevirtual #4 // Method org/programming/mitra/exercises/OverridingInternalExample$Mammal.speak:()V
Human human = new Human();
human.speak(); // Output - Hello
// 36: invokevirtual #7 // Method org/programming/mitra/exercises/OverridingInternalExample$Human.speak:()V
human.speak("Hindi"); // Output - Namaste
// 42: invokevirtual #9 // Method org/programming/mitra/exercises/OverridingInternalExample$Human.speak:(Ljava/lang/String;)V
}
}
我们可以通过两种方式来回答这个问题:逻辑方式和物理方式,让我们看一下逻辑方式。
Logical Way
从逻辑上讲,在编译阶段可以从引用类型考虑调用方法。 但是在执行时,将从引用所在的对象中调用method。
例如在人类哺乳动物.speak();行编译器会说哺乳动物.speak()被打电话是因为人类哺乳动物是类型哺乳动物。 但是在执行过程中,JVM知道人类哺乳动物抱着一个人类如此反对Human.speak()会被打来的。
好吧,这很简单,直到我们仅将其保持在概念水平上为止。 一旦我们怀疑JVM如何在内部处理所有这些情况? 或JVM如何计算应调用的方法。
此外,我们知道重载方法不称为多态方法,而是在编译时解决的,这就是为什么有时方法重载也被称为**编译时多态性或早期/静态绑定**的原因。
但是重写的方法在运行时会得到解决,因为编译器不知道,我们分配给引用的对象是否重写了该方法。
Physical Way
在本节中,我们将尝试找出所有上述语句的物理证明,并找到它们,我们将读取程序的字节码,我们可以通过执行该字节码来执行javap -冗长 OverridingInternalExample。 通过使用-冗长选项,我们将获得与我们的Java程序相同的描述性字节码。
上面的命令分两部分显示了字节码
1.常量池:包含执行程序所需的几乎所有内容,例如 方法参考(#Methodref),类对象(#类),字符串文字(#串),请点击图片进行缩放。
2.程序的字节码:可执行的字节码说明,请单击图像放大。
Why Method overloading is called static binding
在上述代码中human哺乳动物.speak()编译器会说说话()从接到哺乳动物但是在执行时会从对象中调用human哺乳动物持有,这是对象人的类。
通过查看上面的代码和图像,我们可以看到humanMammal.speak(),human.speak()和human.speak(“印地语”)完全不同,因为编译器能够基于类引用对其进行区分。
因此,在方法重载的情况下,编译器能够在编译时识别字节码指令和方法的地址,这就是为什么它也被称为静态绑定或编译时多态。
Why Method overriding is called dynamic binding
的字节码any哺乳动物.speak()和human哺乳动物.speak()都是一样的invokevirtual#4 //方法org / programming / mitra / exercises / OverridingInternalExample $ 哺乳动物.speak :()V),因为根据编译器,两种方法都被调用哺乳动物参考。
因此,现在出现的问题是,如果两个方法调用都具有相同的字节码,那么JVM如何知道要调用哪个方法?
好吧,答案隐藏在字节码本身中,它是虚拟调用指令,根据JVM规范
invokevirtual调用对象的实例方法,并根据对象的(虚拟)类型进行分派。 这是Java编程语言中的常规方法分派。
JVM使用虚拟调用指令以调用等效于C ++虚拟方法的Java。 在C ++中,如果要覆盖另一个类中的一个方法,则需要将其声明为虚拟, But in Java, all methods are 虚拟 by default (except final and static methods) because we can override every method in the child class.
运作方式虚拟调用接受指向方法引用调用的指针(#4常量池的索引)
invokevirtual #4 // Method org/programming/mitra/exercises/OverridingInternalExample$Mammal.speak:()V
该方法引用#4再次引用了方法名称和类引用
#4 = Methodref #2.#27 // org/programming/mitra/exercises/OverridingInternalExample$Mammal.speak:()V
#2 = Class #25 // org/programming/mitra/exercises/OverridingInternalExample$Mammal
#25 = Utf8 org/programming/mitra/exercises/OverridingInternalExample$Mammal
#27 = NameAndType #35:#17 // speak:()V
#35 = Utf8 speak
#17 = Utf8
所有这些引用共同用于获取对要在其中找到该方法的方法和类的引用。 JVM规范中也提到了这一点
Java虚拟机不要求对象4具有任何特定的内部结构。
和书签4状态
在Oracle对Java虚拟机的一些实现中,对类实例的引用是指向本身就是一对指针的句柄的指针:一个指向包含对象方法的表的指针,以及指向表示对象的Class对象的指针 对象的类型,另一个分配给从堆为对象数据分配的内存。
这意味着每个引用变量都包含两个隐藏的指针
- 指向再次包含对象方法的表的指针和指向Class对象的指针。 例如 [speak(),speak(String)类对象]指向堆中为该对象的数据分配的内存的指针,例如 实例变量的值。
但是问题又来了虚拟调用在内部执行此操作? 嗯,没人能回答这个问题,因为它取决于JVM的实现,并且因JVM而异。
And from the above statements, we can conclude that an object reference indirectly holds a reference/pointer to a table which holds all the method references of that object. Java has borrowed this concept from C++ and this table is known by various names which such as virtual method table ( VMT ), virtual function table ( ** vftable** ), virtual table ( vtable ), dispatch table.
我们不确定如何虚拟表在Java中实现,因为它依赖JVM。 但是我们可以预期它将遵循与C ++相同的策略,其中虚拟表是一个类似于数组的结构,其中包含方法名称及其在数组索引上的引用。 每当JVM尝试执行虚拟方法时,它总是会询问虚拟表为它的地址。
There is only one vtable
per class means it is unique and the same for all objects of a class similar to Class
object. I have discussed more on Class
object in my articles Why an outer Java class can’t be static and Why Java is Purely Object-Oriented Language Or Why Not.
所以只有一个虚拟表对于宾语该类包含所有11个方法(如果不计算registerNatives的话)以及对它们各自方法主体的引用。
JVM加载时哺乳动物类存入内存会创建一个类为此对象并创建一个虚拟表 which contains all the methods from the 虚拟表 of Object class with the same references (Because 哺乳动物不会覆盖Object的任何方法),并为说话方法。
现在轮到人的类,现在JVM将复制来自虚拟表的哺乳动物上课虚拟表的人的 and adds a new entry for the overloaded version的说话(字符串)。
JVM知道人的类重写了两种方法,一种是toString()从宾语第二个是斑点()从哺乳动物。 现在,而不是使用更新的引用为这些方法创建新条目。 JVM将修改对早先存在的相同索引上已经存在的方法的引用,并将保留相同的方法名称。
的虚拟调用使JVM处理方法引用处的值#4,而不是作为地址,而是作为要在其中查找的方法的名称虚拟表对于当前对象。
我希望现在可以清楚地知道JVM是如何混合的恒定池条目和虚拟表得出要调用的方法的结论。
You can find the complete code on this Github Repository and please feel free to provide your valuable feedback.