Maison >Java >javaDidacticiel >Explication détaillée de l'histoire intérieure du mécanisme de liaison dynamique Java (image)
Pendant le processus d'appel d'une méthode Java, comment la JVM sait-elle quel code source de méthode de classe est appelé ? Quelle est l’histoire intérieure ici ? Dans cet article, nous révélerons le mécanisme de liaison statique (statique) et de liaison dynamique (liaison automatique) des appels de méthode JVM.
//被调用的类 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(); //调用静态方法 } }
L'instruction (Father.f1()) dans le code source ci-dessus qui exécute l'appel de méthode est compilée par le compilateur en une instruction : invoquerstatique #13. Jetons un coup d'œil à la façon dont la JVM gère cette instruction
(1) #13 dans l'instruction fait référence à l'entrée d'index de la 13ème table constante dans le pool de constantes de la classe StaticCall (pour plus de détails sur le pool de constantes , voir « Contenu du fichier de classe et pool constant 》). Cette table constante (CONSTATN_Methodref_info) enregistre la référence symbolique des informations de la méthode f1 (y compris le nom de la classe, le nom de la méthode et le type de retour de f1). La JVM trouvera d'abord le nom complet de la classe où la méthode f1 est basée sur cette référence symbolique : hr.test.Father
(2) Ensuite la JVM chargera, liera et initialisera la classe Father; ;
(3) Recherchez ensuite l'adresse directe de la méthode f1() dans la zone de méthode où se trouve la classe Père, et enregistrez cette adresse directe dans la table des constantes d'index 13 dans la constante. pool de la classe StaticCall. Ce processus est appelé analyse de pool constant Lorsque Father.f1() sera à nouveau appelé dans le futur, le bytecode de la méthode f1 sera trouvé directement
(4) Terminer la constante de classe StaticCall Une fois la table constante de l'élément d'index de pool 13 analysée, la JVM peut appeler la méthode f1() et commencer à interpréter et à exécuter les instructions de la méthode f1().
Grâce au processus ci-dessus, nous avons constaté qu'après avoir analysé le pool de constantes, la JVM peut déterminer où se trouve la méthode f1() à appeler dans la mémoire. En fait, ces informations ont été enregistrées dans le pool constant de la classe StaticCall lors de la phase de compilation. Cette façon de déterminer quelle méthode appeler pendant la phase de compilation est appelée mécanisme de liaison statique .
À l'exception des méthodes statiques modifiées par static, toutes les méthodes privées modifiées par private et les méthodes modifiées par final qui ne peuvent pas être remplacées par des sous-classes seront compilées dans des instructions d'invocationstatique. De plus, les méthodes d'initialisation 7e51f00a783d7eb8f68358439dee7daf et 583d030be372af71281df966e84181a5 de toutes les classes seront compilées dans des instructions d'invocation spéciales. La JVM utilisera un mécanisme de liaison statique pour appeler ces méthodes en douceur.
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() } }
Il y a trois concepts importants dans le code source ci-dessus : Polymorphisme (polymorphisme) , Méthode couverture, Surcharge de méthode . Les résultats imprimés sont clairs pour tout le monde, mais comment la JVM sait-elle que f.f1() appelle la méthode de la sous-classe Sun au lieu de la méthode de Father ? Avant d'expliquer ce problème, parlons d'abord brièvement d'une structure de données très importante gérée par la JVM - table de méthodes .
Lorsque la JVM chargera une classe, elle stockera de nombreuses informations pour cette classe dans la zone des méthodes (voir "Architecture de la machine virtuelle Java" pour plus de détails 》). Il existe une structure de données appelée table de méthodes. Il enregistre l'adresse directe en mémoire du bytecode de méthode visible de la classe actuelle et de toutes ses superclasses sous forme de tableau . L'image suivante est la table des méthodes dans la zone de méthode des classes Père et Soleil dans le code source ci-dessus :
La table des méthodes dans l'image ci-dessus a deux caractéristiques : (1) Sous-classe Dans la table des méthodes, hérite des méthodes de la classe parent, telles que Father extends Object. (2) La même méthode (même signature de méthode : nom de la méthode et liste de paramètres) a le même index dans la table des méthodes de toutes les classes. Par exemple, f1() dans la table des méthodes Father et f1() dans la table des méthodes Son sont tous deux situés dans le 11ème élément de leurs tables de méthodes respectives.
Pour le code source ci-dessus, le compilateur compilera d'abord la méthode principale dans les instructions de bytecode suivantes :
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
Le processus d'appel détaillé de l'instruction invocationvirtual est le suivant :
(1) #15 dans l'instruction Invokevirtual fait référence à l'élément d'index de la 15ème table constante dans le pool de constantes de la classe AutoCall. Cette table constante (CONSTATN_Methodref_info) enregistre la référence symbolique des informations de la méthode f1 (y compris le nom de la classe, le nom de la méthode et le type de retour de f1). La JVM trouvera d'abord le nom complet de la classe qui appelle la méthode f1 en fonction de cette référence symbolique : hr.test.Father. En effet, l'objet père de la classe appelant la méthode f1 est déclaré comme type Père.
(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) 所有私有方法、静态方法、构造器及初始化方法583d030be372af71281df966e84181a5都是采用静态绑定机制。在编译器阶段就已经指明了调用方法在常量池中的符号引用,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!”); } }
Ce qui précède est le contenu détaillé de. pour plus d'informations, suivez d'autres articles connexes sur le site Web de PHP en chinois!