Woher weiß die JVM während des Aufrufs einer Java-Methode, welcher Methodenquellcode der Klasse aufgerufen wird? Was ist hier die Insider-Geschichte? In diesem Artikel werden wir den statischen (statische Bindung) und den dynamischen Bindungsmechanismus (automatische Bindung) von JVM-Methodenaufrufen enthüllen.
//被调用的类 package hr.test; class Father{ public static void f1(){ System.out.println("Father— f1()"); } } //调用静态方法 import hr.test.Father; public class StaticCall{ public static void main(){ Father.f1(); //调用静态方法 } }
Die Anweisung (Father.f1()) im obigen Quellcode, die den Methodenaufruf ausführt, wird vom Compiler in eine Anweisung kompiliert: invokestatic #13. Schauen wir uns an, wie die JVM diese Anweisung verarbeitet
(1) #13 in der Anweisung bezieht sich auf den Indexeintrag der 13. Konstantentabelle im Konstantenpool der StaticCall-Klasse (für Details zum Konstantenpool). , siehe „Inhalte der Klassendatei und Konstantenpool“. 》). Diese Konstantentabelle (CONSTATN_Methodref_info) zeichnet die symbolische Referenz der Informationen zur Methode f1 auf (einschließlich Klassenname, Methodenname und Rückgabetyp von f1). Die JVM findet zunächst den vollständig qualifizierten Namen der Klasse, in der die Methode f1 auf dieser symbolischen Referenz basiert:
(2) Dann lädt, verknüpft und initialisiert die JVM die Vaterklasse ;
(3) Suchen Sie dann die direkte Adresse der f1()-Methode im Methodenbereich, in dem sich die Vaterklasse befindet, und zeichnen Sie diese direkte Adresse in der Konstantentabelle mit Index 13 auf Pool der StaticCall-Klasse. Dieser Vorgang wird als konstantes Pool-Parsing bezeichnet. Wenn Father.f1() in Zukunft erneut aufgerufen wird, wird der Bytecode der f1-Methode direkt gefunden
(4) Die StaticCall-Klassenkonstante wurde fertiggestellt. Nachdem die Konstantentabelle des Poolindexelements 13 analysiert wurde, kann die JVM die f1()-Methode aufrufen und mit der Interpretation und Ausführung der Anweisungen in der f1()-Methode beginnen.
Durch den obigen Prozess haben wir festgestellt, dass die JVM nach dem Parsen des Konstantenpools feststellen kann, wo sich die aufzurufende f1()-Methode im Speicher befindet. Tatsächlich wurden diese Informationen während der Kompilierungsphase im Konstantenpool der StaticCall-Klasse aufgezeichnet. Diese Art der Bestimmung, welche Methode während der Kompilierungsphase aufgerufen werden soll, wird als statischer Bindungsmechanismus bezeichnet.
Mit Ausnahme der durch static geänderten statischen Methoden werden alle durch private und durch final geänderten privaten Methoden, die nicht von Unterklassen überschrieben werden dürfen, in invokestatic-Anweisungen kompiliert. Darüber hinaus werden die Initialisierungsmethoden
package hr.test; //被调用的父类 class Father{ public void f1(){ System.out.println("father-f1()"); } public void f1(int i){ System.out.println("father-f1() para-int "+i); } } //被调用的子类 class Son extends Father{ public void f1(){ //覆盖父类的方法 System.out.println("Son-f1()"); } public void f1(char c){ System.out.println("Son-s1() para-char "+c); } } //调用方法 import hr.test.*; public class AutoCall{ public static void main(String[] args){ Father father=new Son(); //多态 father.f1(); //打印结果: Son-f1() } }
Im obigen Quellcode gibt es drei wichtige Konzepte: Polymorphismus (Polymorphismus) , Methode Abdeckung, Methodenüberladung . Die gedruckten Ergebnisse sind für jeden klar, aber woher weiß die JVM, dass f.f1() die Methode in der Unterklasse Sun anstelle der Methode in Father aufruft? Bevor wir dieses Problem erklären, wollen wir zunächst kurz auf eine sehr wichtige Datenstruktur eingehen, die von der JVM verwaltet wird – Methodentabelle .
Wenn die JVM eine Klasse lädt, speichert sie viele Informationen für diese Klasse im Methodenbereich (Einzelheiten finden Sie unter „Java Virtual Machine Architecture“. 》). Es gibt eine Datenstruktur, die als Methodentabelle bezeichnet wird. Es zeichnet die direkte Adresse im Speicher des sichtbaren Methodenbytecodes der aktuellen Klasse und aller ihrer Oberklassen in Form eines Arrays auf . Das folgende Bild ist die Methodentabelle im Methodenbereich der Vater- und Sonnenklassen im obigen Quellcode:
Die Methodentabelle im obigen Bild weist zwei Merkmale auf : (1) Unterklasse In der Methodentabelle erbt die Methoden der übergeordneten Klasse, z. B. „Vater erweitert“ Objekt. (2) Dieselbe Methode (gleiche Methodensignatur: Methodenname und Parameterliste) hat denselben Index in der Methodentabelle aller Klassen. Beispielsweise befinden sich f1() in der Methodentabelle „Vater“ und f1() in der Methodentabelle „Sohn“ beide im 11. Element ihrer jeweiligen Methodentabellen.
Für den obigen Quellcode kompiliert der Compiler zunächst die Hauptmethode in die folgenden Bytecode-Anweisungen:
0 new hr.test.Son [13] //在堆中开辟一个Son对象的内存空间,并将对象引用压入操作数栈 3 dup 4 invokespecial #7 [15] // 调用初始化方法来初始化堆中的Son对象 7 astore_1 //弹出操作数栈的Son对象引用压入局部变量1中 8 aload_1 //取出局部变量1中的对象引用压入操作数栈 9 invokevirtual #15 //调用f1()方法 12 return
Der detaillierte Aufrufvorgang der invokevirtual-Anweisung ist wie folgt:
(1) #15 in der invokevirtual-Anweisung bezieht sich auf das Indexelement der 15. Konstantentabelle im Konstantenpool der AutoCall-Klasse. Diese Konstantentabelle (CONSTATN_Methodref_info) zeichnet die symbolische Referenz der Informationen zur Methode f1 auf (einschließlich Klassenname, Methodenname und Rückgabetyp von f1). Die JVM findet zunächst den vollständig qualifizierten Namen der Klasse, die die Methode f1 aufruft, basierend auf dieser symbolischen Referenz: hr.test.Father. Dies liegt daran, dass der Objektvater der Klasse, die die Methode f1 aufruft, als Vatertyp deklariert ist.
(2) 在Father类型的方法表中查找方法f1,如果找到,则将方法f1在方法表中的索引项11(如上图)记录到AutoCall类的常量池中第15个常量表中(常量池解析 )。这里有一点要注意:如果Father类型方法表中没有方法f1,那么即使Son类型中方法表有,编译的时候也通过不了。因为调用方法f1的类的对象father的声明为Father类型。
(3) 在调用invokevirtual指令前有一个aload_1指令,它会将开始创建在堆中的Son对象的引用压入操作数栈。然后invokevirtual指令会根据这个Son对象的引用首先找到堆中的Son对象,然后进一步找到Son对象所属类型的方法表。过程如下图所示:
(4) 这是通过第(2)步中解析完成的#15常量表中的方法表的索引项11,可以定位到Son类型方法表中的方法f1(),然后通过直接地址找到该方法字节码所在的内存空间。
很明显,根据对象(father)的声明类型(Father)还不能够确定调用方法f1的位置,必须根据father在堆中实际创建的对象类型Son来确定f1方法所在的位置。这种在程序运行过程中,通过动态创建的对象的方法表来定位方法的方式,我们叫做 动态绑定机制 。
上面的过程很清楚的反映出在方法覆盖的多态调用的情况下,JVM是如何定位到准确的方法的。但是下面的调用方法JVM是如何定位的呢?(仍然使用上面代码中的Father和Son类型)
public class AutoCall{ public static void main(String[] args){ Father father=new Son(); char c='a'; father.f1(c); //打印结果:father-f1() para-int 97 } }
问题是Fahter类型中并没有方法签名为f1(char)的方法呀。但打印结果显示JVM调用了Father类型中的f1(int)方法,并没有调用到Son类型中的f1(char)方法。
根据上面详细阐述的调用过程,首先可以明确的是:JVM首先是根据对象father声明的类型Father来解析常量池的(也就是用Father方法表中的索引项来代替常量池中的符号引用)。如果Father中没有匹配到”合适” 的方法,就无法进行常量池解析,这在编译阶段就通过不了。
那么什么叫”合适”的方法呢?当然,方法签名完全一样的方法自然是合适的。但是如果方法中的参数类型在声明的类型中并不能找到呢?比如上面的代码中调用father.f1(char),Father类型并没有f1(char)的方法签名。实际上,JVM会找到一种“凑合”的办法,就是通过 参数的自动转型 来找 到“合适”的 方法。比如char可以通过自动转型成int,那么Father类中就可以匹配到这个方法了 (关于Java的自动转型问题可以参见《【解惑】Java类型间的转型》)。但是还有一个问题,如果通过自动转型发现可以“凑合”出两个方法的话怎么办?比如下面的代码:
class Father{ public void f1(Object o){ System.out.println("Object"); } public void f1(double[] d){ System.out.println("double[]"); } } public class Demo{ public static void main(String[] args) { new Father().f1(null); //打印结果: double[] } }
null可以引用于任何的引用类型,那么JVM如何确定“合适”的方法呢。一个很重要的标准就是:如果一个方法可以接受传递给另一个方法的任何参数,那么第一个方法就相对不合适。比如上面的代码: 任何传递给f1(double[])方法的参数都可以传递给f1(Object)方法,而反之却不行,那么f1(double[])方法就更合适。因此JVM就会调用这个更合适的方法。
(1) 所有私有方法、静态方法、构造器及初始化方法
都是采用静态绑定机制。在编译器阶段就已经指明了调用方法在常量池中的符号引用,JVM运行的时候只需要进行一次常量池解析即可。 (2) 类对象方法的调用必须在运行过程中采用动态绑定机制。
首先,根据对象的声明类型(对象引用的类型)找到“合适”的方法。具体步骤如下:
① 如果能在声明类型中匹配到方法签名完全一样(参数类型一致)的方法,那么这个方法是最合适的。
② 在第①条不能满足的情况下,寻找可以“凑合”的方法。标准就是通过将参数类型进行自动转型之后再进行匹配。如果匹配到多个自动转型后的方法签名f(A)和f(B),则用下面的标准来确定合适的方法:传递给f(A)方法的参数都可以传递给f(B),则f(A)最合适。反之f(B)最合适 。
③ 如果仍然在声明类型中找不到“合适”的方法,则编译阶段就无法通过。
然后,根据在堆中创建对象的实际类型找到对应的方法表,从中确定具体的方法在内存中的位置。
一个实例方法可以覆写(override)在其超类中可访问到的具有相同签名的所有实例方法,从而使能了动态分派(dynamic dispatch);换句话说,VM将基于实例的运行期类型来选择要调用的覆写方法。覆写是面向对象编程技术的基础,并且是唯一没有被普遍劝阻的名字重用形式:
class Base{ public void f(){} } class Derived extends Base{ public void f(){} }
一个域、静态方法或成员类型可以分别隐藏(hide)在其超类中可访问到的具有相同名字(对方法而言就是相同的方法签名)的所有域、静态方法或成员类型。隐藏一个成员将阻止其被继承。
class Base{ public static void f(){} } class Derived extends Base { private static void f(){} //hides Base. f() }
在某个类中的方法可以重载(overload)另一个方法,只要它们具有相同的名字和不同的签名。由调用所指定的重载方法是在编译期选定的。
class CircuitBreaker{ public void f (int i){} //int overloading public void f(String s){} //String overloading }
一个变量、方法或类型可以分别遮蔽(shadow)在一个闭合的文本范围内的具有相同名字的所有变量、方法或类型。如果一个实体被遮蔽了,那么你用它的简单名是无法引用到它的;根据实体的不同,有时你根本就无法引用到它。
class WhoKnows{ static String sentence=”I don't know.”; public static void main(String[] args〕{ String sentence=”I don't know.”; //shadows static field System.out. println (sentence); // prints local variable } }
尽管遮蔽通常是被劝阻的,但是有一种通用的惯用法确实涉及遮蔽。构造器经常将来自其所在类的某个域名重用为一个参数,以传递这个命名域的值。这种惯用法并不是没有风险,但是大多数Java程序员都认为这种风格带来的实惠要超过
其风险:
class Belt{ private find int size ; //Parameter shadows Belt. size public Belt (int size){ this. size=size; } }
一个变量可以遮掩具有相同名字的一个类型,只要它们都在同一个范围内:如果这个名字被用于变量与类型都被许可的范围,那么它将引用到变量上。相似地,一个变量或一个类型可以遮掩一个包。遮掩是唯一一种两个名字位于不同的名字空间的名字重用形式,这些名字空间包括:变量、包、方法或类型。如果一个类型或一个包被遮掩了,那么你不能通过其简单名引用到它,除非是在这样一个上下文环境中,即语法只允许在其名字空间中出现一种名字。遵守命名习惯就可以极大地消除产生遮掩的可能性:
public class Obscure{ static String System;// Obscures type java.lang.System public static void main(String[] args) // Next line won't compile:System refers to static field System. out. println(“hello, obscure world!”); } }
Das obige ist der detaillierte Inhalt vonDetaillierte Erläuterung der Insider-Geschichte des dynamischen Java-Bindungsmechanismus (Bild). Für weitere Informationen folgen Sie bitte anderen verwandten Artikeln auf der PHP chinesischen Website!