java符號(hào)的定位與解析(案例+源碼解析)

字節(jié)碼的基本結(jié)構(gòu)

一個(gè)普通類(lèi)的java代碼

public class Parent {
  public void sayHi(){
  }
}

編譯原理是這么描述編譯的:將某一種語(yǔ)言(源語(yǔ)言)編寫(xiě)的程序,翻譯成為一個(gè)等價(jià)的、用另一種語(yǔ)言(目標(biāo)語(yǔ)言)編寫(xiě)的程序。那么對(duì)于java而言,就是將java代碼翻譯成字節(jié)碼,那么上面這個(gè)簡(jiǎn)單的類(lèi),字節(jié)碼是怎么描述的呢,使用 javap -v Parent.class可以看到字節(jié)碼的相應(yīng)結(jié)構(gòu)

public class Parent
  minor version: 0
  major version: 52
  flags: ACC_PUBLIC, ACC_SUPER
Constant pool: 常量池
   #1 = Methodref          #3.#11         // java/lang/Object."<init>":()V
   #2 = Class              #12            // Parent
   #3 = Class              #13            // java/lang/Object
   #4 = Utf8               <init>
   #5 = Utf8               ()V
   #6 = Utf8               Code
   #7 = Utf8               LineNumberTable
   #8 = Utf8               sayHi
   #9 = Utf8               SourceFile
  #10 = Utf8               Parent.java
  #11 = NameAndType        #4:#5          // "<init>":()V
  #12 = Utf8               Parent
  #13 = Utf8               java/lang/Object
{ 方法表
  public Parent();
    descriptor: ()V
    flags: ACC_PUBLIC
    Code:
      stack=1, locals=1, args_size=1
         0: aload_0
         1: invokespecial #1                  // Method java/lang/Object."<init>":()V
         4: return
      LineNumberTable:
        line 1: 0

  public void sayHi();
    descriptor: ()V
    flags: ACC_PUBLIC
    Code:
      stack=0, locals=1, args_size=1
         0: return
      LineNumberTable:
        line 3: 0
}

其中,constant pool代表常量池,它可以理解為class文件中的資源倉(cāng)庫(kù),常量池中主要存放兩大類(lèi)常量,字面量和符號(hào)引用,字面量比較接近于java層面的常量概念,如文本字符串,聲明為final的常量值等,符號(hào)引用則是屬于編譯原理方面的內(nèi)容,本例中類(lèi)型為Utf8的就是字面量,其他都是符號(hào)引用

Constant pool:
   #2 = Class              #12            // Parent
   #3 = Class              #13            // java/lang/Object
  #11 = NameAndType        #4:#5          // "<init>":()
  #12 = Utf8               Parent
  #13 = Utf8               java/lang/Object

首先來(lái)看這部分常量,常量#2和#3是是符號(hào)引用的第一種類(lèi)型: 類(lèi)和接口的全限定名,常量#2是本類(lèi)的全限定名(Parent),常量#3是父類(lèi)的全限定名(java/lang/Object),這樣字節(jié)碼就將繼承關(guān)系簡(jiǎn)單的描述了出來(lái)

再看第一個(gè)常量#1,這是另一個(gè)類(lèi)型的符號(hào)引用:方法的名稱(chēng)和描述符

#1 = Methodref          #3.#11         // java/lang/Object."<init>":()V
#3 = Class              #13            // java/lang/Object
#4 = Utf8               <init>
#5 = Utf8               ()V
#11 = NameAndType        #4:#5          // "<init>":()

方法的符號(hào)引用比較有意思,可以看到,#1是由#3.#11組合而來(lái),簡(jiǎn)單拼一下就是 #3.#11=Class(#13). NameAndType(#4:#5) = java/lang/Object."<init>":()V,翻譯一下就是類(lèi)java/lang/Object(父類(lèi))的,方法名為<init>的,返回值為void的,沒(méi)有入?yún)⒌姆?hào)引用。
對(duì)于jvm而言,如果要調(diào)用一個(gè)方法,只能根據(jù)上述的符號(hào)引用(java/lang/Object."<init>":()V)來(lái)找到這個(gè)方法,所以必須保證它的精確性和唯一性,本例中,通過(guò)類(lèi)的全限定名、方法名、返回值、入?yún)⒘斜?精確描述了一個(gè)方法

那么方法的符號(hào)引用會(huì)在那部分使用到呢,在如下的方法表中,有個(gè)Code區(qū),里面存放是java代碼編譯而來(lái)的字節(jié)碼指令

  public Parent();
    descriptor: ()V
    flags: ACC_PUBLIC
    Code:
      stack=1, locals=1, args_size=1
         0: aload_0
         1: invokespecial #1                  // Method java/lang/Object."<init>":()V
         4: return
      LineNumberTable:
        line 1: 0

從方法名可以看出,本方法是編譯器自動(dòng)生成的無(wú)參構(gòu)造方法Parent(),里面 invokespecial #1 這一條指令即調(diào)用常量#1對(duì)應(yīng)的方法引用,即父類(lèi)方法java/lang/Object."<init>":()V, invokespecial會(huì)在后文進(jìn)一步介紹,這里可以簡(jiǎn)單理解為調(diào)用方法的指令

<init>()方法與構(gòu)造方法的關(guān)系

再看這個(gè)指令的細(xì)節(jié),<init>方法是編譯器自動(dòng)生成的方法,名為實(shí)例構(gòu)造器,奇怪的是,構(gòu)造函數(shù)Parent()中只調(diào)用了父類(lèi)的<init>方法,沒(méi)有沒(méi)有調(diào)用自己的<init>方法,那這個(gè)<init>方法和構(gòu)造方法有什么關(guān)系呢,是不是包含關(guān)系?可以驗(yàn)證一下

public class Parent1 {
  public Parent1(){
        System.out.println("hello world");
  }

  public void sayHi(){
    Parent1 parent = new Parent1();
  }
}

給類(lèi)Parent1顯式定義一個(gè)無(wú)參構(gòu)造函數(shù)以及一個(gè)成員方法,javap -v一下:

public void sayHi();
    descriptor: ()V
    flags: ACC_PUBLIC
    Code:
      stack=2, locals=2, args_size=1
         0: new           #5                  // class Parent1
         3: dup
         4: invokespecial #6                  // Method "<init>":()V
         7: astore_1
         8: return

可以看到,sayHi()這個(gè)方法,在new Parent1()時(shí),只有對(duì)Parent1的<init>方法的調(diào)用,沒(méi)有對(duì)無(wú)參構(gòu)造方法Parent1()方法的調(diào)用,那么<init>方法必然包含了執(zhí)行構(gòu)造方法

 public Parent1();
   descriptor: ()V
   flags: ACC_PUBLIC
   Code:
     stack=2, locals=1, args_size=1
        0: aload_0
        1: invokespecial #1                  // Method java/lang/Object."<init>":()V
        4: getstatic     #2                  // Field java/lang/System.out:Ljava/io/PrintStream;
        7: ldc           #3                  // String hello world
        9: invokevirtual #4                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
       12: return

再看顯式聲明的構(gòu)造函數(shù)Parent1(),父類(lèi)的<init>方法在具體邏輯(hello world)之前,
那么可以得出結(jié)論:<init>方法會(huì)在實(shí)例化的時(shí)候調(diào)用,且<init>方法包含了構(gòu)造方法的邏輯,而構(gòu)造方法中,在其他邏輯之前顯式調(diào)用了父類(lèi)的<init>方法,這與我們的java常識(shí)也是相符的

符號(hào)引用如何解析為直接引用

通過(guò)以上的例子,應(yīng)該對(duì)類(lèi)結(jié)構(gòu)和字節(jié)碼有了基本的認(rèn)識(shí),可能會(huì)有一個(gè)疑惑,字節(jié)碼中的常量池里存放的都只是符號(hào)引用,那jvm是如何通過(guò)符號(hào)引用定位到真正的地址呢
這也是Java與c不同的地方,c語(yǔ)言將.c源文件編譯成.o文件后,.o文件中的引用直接就是使用的分配好的虛擬地址,而java的class文件中引用就僅僅是一個(gè)描述性的符號(hào),并沒(méi)有保存最終的內(nèi)存布局信息,那么引出兩個(gè)問(wèn)題,java編譯器如何保證能夠讓虛擬機(jī)準(zhǔn)確定位到符號(hào)引用指向的實(shí)際引用,以及虛擬機(jī)在什么時(shí)候?qū)⒎?hào)引用轉(zhuǎn)為實(shí)際引用

符號(hào)引用:是以一組符號(hào)來(lái)描述所引用的目標(biāo),符號(hào)可以是任何形式的字面量,只要使用時(shí)能無(wú)歧義地定位到目標(biāo)即可. 符號(hào)引用的目標(biāo)不一定要加載到內(nèi)存中.

直接引用:是直接指向目標(biāo)的指針、相對(duì)偏移量或是一個(gè)能間接定位到目標(biāo)的句柄. 如果有了直接引用,那引用的目標(biāo)必定存在于內(nèi)存中.

第一個(gè)問(wèn)題上文也提到了,符號(hào)引用包含足夠的信息,以供jvm實(shí)際使用時(shí)可以找到相應(yīng)的位置,比如:“java/io/PrintStream.println:(Ljava/lang/String;)V”,虛擬機(jī)就會(huì)將其轉(zhuǎn)為實(shí)際引用,這個(gè)過(guò)程叫做解析,但是解析的時(shí)機(jī)并不是固定的

虛擬機(jī)規(guī)范中并沒(méi)有明確規(guī)定解析階段發(fā)生的具體時(shí)間,只要求了在執(zhí)行 anewarray、checkcast、getfield、getstatic、instanceof、invokedynamic、invokeinterface、invokespecial、invokestatic、invokevirtual、ldc、ldc_w、multianewarray、new、putfield、putstatic 用于操作符號(hào)引用的字節(jié)碼指令前,先對(duì)它們所使用的符號(hào)進(jìn)行解析. 所以虛擬機(jī)實(shí)現(xiàn)可以根據(jù)需要來(lái)判斷到底是在類(lèi)被加載時(shí)就對(duì)常量池中的符號(hào)進(jìn)行解析,還是等到一個(gè)符號(hào)將要被使用前才解析它.

類(lèi)加載過(guò)程.png

以上是類(lèi)加載的流程,解析是連接中的一個(gè)部分,也是本文的重點(diǎn),將通過(guò)擼hotspot的源碼來(lái)配合理解這個(gè)過(guò)程,源代碼在下圖的這個(gè)路徑下

連接源代碼路徑.png

首先看類(lèi)連接的時(shí)機(jī),以下是類(lèi)初始化函數(shù)的部分代碼,可以看出,第一行邏輯就是進(jìn)行類(lèi)解析,解析完之后才會(huì)繼續(xù)初始化,與常識(shí)相符(先連接再初始化)

void InstanceKlass::initialize_impl(instanceKlassHandle this_oop, TRAPS) {
 // Make sure klass is linked (verified) before initialization
 // A class could already be verified, since it has been reflected upon.
 this_oop->link_class(CHECK);
 ...
}

順便看下InstanceKlass是個(gè)什么東西,跟類(lèi)有什么關(guān)系

// An InstanceKlass is the VM level representation of a Java class.
// It contains all information needed for at class at execution runtime.

//  InstanceKlass layout:
//    [C++ vtbl pointer           ] Klass
//    [subtype cache              ] Klass
//    [instance size              ] Klass
//    [java mirror                ] Klass
//    [super                      ] Klass
//    [access_flags               ] Klass
//    [name                       ] Klass
//    [first subklass             ] Klass
//    [next sibling               ] Klass
//    [array klasses              ]
//    [methods                    ]
//    [local interfaces           ]
//    [transitive interfaces      ]
//    [fields                     ]
//    [constants                  ]
//    [class loader               ]
//    [source file name           ]
//    [inner classes              ]
//    [static field size          ]
//    [nonstatic field size       ]
//    [static oop fields size     ]
//    [nonstatic oop maps size    ]
//    [has finalize method        ]
//    [deoptimization mark bit    ]
//    [initialization state       ]
//    [initializing thread        ]
//    [Java vtable length         ]
//    [oop map cache (stack maps) ]
//    [EMBEDDED Java vtable             ] size in words = vtable_len
//    [EMBEDDED nonstatic oop-map blocks] size in words = nonstatic_oop_map_size
//      The embedded nonstatic oop-map blocks are short pairs (offset, length)
//      indicating where oops are located in instances of this klass.
//    [EMBEDDED implementor of the interface] only exist for interface
//    [EMBEDDED host klass        ] only exist for an anonymous class (JSR 292 en

InstanceKlass存著Java類(lèi)型的名字、繼承關(guān)系、實(shí)現(xiàn)接口關(guān)系,字段信息,方法信息,運(yùn)行時(shí)常量池的指針,還有內(nèi)嵌的虛方法表(vtable)、接口方法表(itable)和記錄對(duì)象里什么位置上有GC會(huì)關(guān)心的指針(oop map)等等。
是給VM內(nèi)部用的,并不直接暴露給Java層;InstanceKlass不是java.lang.Class的實(shí)例。

再看看類(lèi)連接的邏輯:

bool InstanceKlass::link_class_impl(
   instanceKlassHandle this_oop, bool throw_verifyerror, TRAPS) {
 // check for error state
 if (this_oop->is_in_error_state()) {
   ResourceMark rm(THREAD);
   THROW_MSG_(vmSymbols::java_lang_NoClassDefFoundError(),
              this_oop->external_name(), false);
 }
 // return if already verified
 if (this_oop->is_linked()) {
   return true;
 }

 // Timing
 // timer handles recursion
 assert(THREAD->is_Java_thread(), "non-JavaThread in link_class_impl");
 JavaThread* jt = (JavaThread*)THREAD;

 // link super class before linking this class
 instanceKlassHandle super(THREAD, this_oop->super());
 if (super.not_null()) {
   if (super->is_interface()) {  // check if super class is an interface
     ResourceMark rm(THREAD);
     Exceptions::fthrow(
       THREAD_AND_LOCATION,
       vmSymbols::java_lang_IncompatibleClassChangeError(),
       "class %s has interface %s as super class",
       this_oop->external_name(),
       super->external_name()
     );
     return false;
   }

   link_class_impl(super, throw_verifyerror, CHECK_false);
 }

 // link all interfaces implemented by this class before linking this class
 Array<Klass*>* interfaces = this_oop->local_interfaces();
 int num_interfaces = interfaces->length();
 for (int index = 0; index < num_interfaces; index++) {
   HandleMark hm(THREAD);
   instanceKlassHandle ih(THREAD, interfaces->at(index));
   link_class_impl(ih, throw_verifyerror, CHECK_false);
 }

 // in case the class is linked in the process of linking its superclasses
 if (this_oop->is_linked()) {
   return true;
 }

注釋比較清晰,比較容易捋出大體的流程,為了直觀一點(diǎn)畫(huà)個(gè)流程圖

連接過(guò)程.png

可以看到,連接過(guò)程中沒(méi)有看到有解析這一步,因?yàn)榻馕龅臅r(shí)機(jī)并不是固定的,解析的邏輯則是交給了linkResolve這個(gè)類(lèi)來(lái)完成,后文會(huì)進(jìn)一步分析,這里先重點(diǎn)搞清楚 初始化vtableitable這一步,vatable也被稱(chēng)為虛方法表,從代碼來(lái)看它初始化的邏輯,這也是java是實(shí)現(xiàn)重寫(xiě)的一個(gè)很重要的步驟

虛方法表 & 重寫(xiě)(源碼分析)

void klassVtable::initialize_vtable(bool checkconstraints, TRAPS) {

 // Note:  Arrays can have intermediate array supers.  Use java_super to skip them.
 KlassHandle super (THREAD, klass()->java_super());
 int nofNewEntries = 0;

 if (PrintVtables && !klass()->oop_is_array()) {
   ResourceMark rm(THREAD);
   tty->print_cr("Initializing: %s", _klass->name()->as_C_string());
 }

#ifdef ASSERT
 oop* end_of_obj = (oop*)_klass() + _klass()->size();
 oop* end_of_vtable = (oop*)&table()[_length];
 assert(end_of_vtable <= end_of_obj, "vtable extends beyond end");
#endif

 if (Universe::is_bootstrapping()) {
   // just clear everything
   for (int i = 0; i < _length; i++) table()[i].clear();
   return;
 }

 int super_vtable_len = initialize_from_super(super);
 if (klass()->oop_is_array()) {
   assert(super_vtable_len == _length, "arrays shouldn't introduce new methods");
 } else {
   assert(_klass->oop_is_instance(), "must be InstanceKlass");

   Array<Method*>* methods = ik()->methods();
   int len = methods->length();
   int initialized = super_vtable_len;

   // Check each of this class's methods against super;
   // if override, replace in copy of super vtable, otherwise append to end
   for (int i = 0; i < len; i++) {
     // update_inherited_vtable can stop for gc - ensure using handles
     HandleMark hm(THREAD);
     assert(methods->at(i)->is_method(), "must be a Method*");
     methodHandle mh(THREAD, methods->at(i));

     bool needs_new_entry = update_inherited_vtable(ik(), mh, super_vtable_len, -1, checkconstraints, CHECK);

     if (needs_new_entry) {
       put_method_at(mh(), initialized);
       mh()->set_vtable_index(initialized); // set primary vtable index
       initialized++;
     }
   }

   // update vtable with default_methods
   Array<Method*>* default_methods = ik()->default_methods();
   if (default_methods != NULL) {
     len = default_methods->length();
     if (len > 0) {
       Array<int>* def_vtable_indices = NULL;
       if ((def_vtable_indices = ik()->default_vtable_indices()) == NULL) {
         def_vtable_indices = ik()->create_new_default_vtable_indices(len, CHECK);
       } else {
         assert(def_vtable_indices->length() == len, "reinit vtable len?");
       }
       for (int i = 0; i < len; i++) {
         HandleMark hm(THREAD);
         assert(default_methods->at(i)->is_method(), "must be a Method*");
         methodHandle mh(THREAD, default_methods->at(i));

         bool needs_new_entry = update_inherited_vtable(ik(), mh, super_vtable_len, i, checkconstraints, CHECK);

         // needs new entry
         if (needs_new_entry) {
           put_method_at(mh(), initialized);
           def_vtable_indices->at_put(i, initialized); //set vtable index
           initialized++;
         }
       }
     }
   }

   // add miranda methods; it will also return the updated initialized
   // Interfaces do not need interface methods in their vtables
   // This includes miranda methods and during later processing, default methods
   if (!ik()->is_interface()) {
     initialized = fill_in_mirandas(initialized);
   }

   // In class hierarchies where the accessibility is not increasing (i.e., going from private ->
   // package_private -> public/protected), the vtable might actually be smaller than our initial
   // calculation.
   assert(initialized <= _length, "vtable initialization failed");
   for(;initialized < _length; initialized++) {
     put_method_at(NULL, initialized);
   }
   NOT_PRODUCT(verify(tty, true));
 }
}

// Update child's copy of super vtable for overrides
// OR return true if a new vtable entry is required.
// Only called for InstanceKlass's, i.e. not for arrays
// If that changed, could not use _klass as handle for klass
bool klassVtable::update_inherited_vtable(InstanceKlass* klass, methodHandle target_method,
                                         int super_vtable_len, int default_index,
                                         bool checkconstraints, TRAPS) {
//省略
}

從以上代碼可以看出,剛開(kāi)始子類(lèi)的虛方法表與父類(lèi)的虛方法表一致,個(gè)數(shù)也一樣,然后再對(duì)子類(lèi)的方法進(jìn)行遍歷,通過(guò)調(diào)用update_inherited_vtable函數(shù)判斷方法是否是對(duì)父類(lèi)的重寫(xiě),如果是,就調(diào)用klassVtable::put_method_at(Method* m, int index)函數(shù)進(jìn)行重寫(xiě)操作,更新子類(lèi) vtable 表中指向父類(lèi)方法的指針,使其指向子類(lèi)中該方法的入口地址。 若該方法并不是對(duì)父類(lèi)方法的重寫(xiě),則會(huì)調(diào)用klassVtable::put_method_at(Method* m, int index)函數(shù)向該 Java 類(lèi)的 vtable 中插入一個(gè)新的指針元素,使其指向該方法的入口地址,即增加一個(gè)新的虛函數(shù)地址,這里要注意一點(diǎn),對(duì)于重寫(xiě)的方法,子類(lèi)和父類(lèi)的方法表的索引值是一致的,這個(gè)特性很關(guān)鍵,后文會(huì)進(jìn)行介紹

多態(tài)的實(shí)現(xiàn)(源碼分析)

在連接階段,虛方法表初始化完成,這個(gè)時(shí)候,再來(lái)看重寫(xiě)和多態(tài)是怎么實(shí)現(xiàn)的,順便將解析的流程也擼一遍,以下是一個(gè)簡(jiǎn)單的例子,子類(lèi)Son3重寫(xiě)了sayHi()方法,并定義了一個(gè)成員方法用到了多態(tài)的特性

public class Parent3 {
 public void sayHi(){
   System.out.println("hi,son");
 }
}

class Son3 extends Parent3{
 public void sayHi(){
     System.out.println("hi,parent");
  }

 public void sayHiTest(){
     Parent3 son3 = new Son3();
     son3.sayHi();
 }

}

顯然若是調(diào)用sayHiTest()方法時(shí),打出的應(yīng)該是"hi,parent",那么,jvm是如何是實(shí)現(xiàn)多態(tài)的呢,首先可以畫(huà)出兩個(gè)類(lèi)的虛方法表


!虛方法表.png

java -v Son3.class一下,

 public void sayHi();
   descriptor: ()V
   flags: ACC_PUBLIC
   Code:
     stack=2, locals=1, args_size=1
        0: getstatic     #2                  // Field java/lang/System.out:Ljava/io/PrintStream;
        3: ldc           #3                  // String hi,parent
        5: invokevirtual #4                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
        8: return
     LineNumberTable:
       line 9: 0
       line 10: 8

 public void sayHiTest();
   descriptor: ()V
   flags: ACC_PUBLIC
   Code:
     stack=2, locals=2, args_size=1
        0: new           #5                  // class Son3
        3: dup
        4: invokespecial #6                  // Method "<init>":()V
        7: astore_1
        8: aload_1
        9: invokevirtual #7                  // Method Parent3.sayHi:()V
       12: return
     LineNumberTable:
       line 13: 0
       line 14: 8
       line 15: 12
}

可以看到 son3.sayHi(); 被編譯成了

9: invokevirtual #7                  // Method Parent3.sayHi:()V

可以看出,編譯器使用了invokevirtual 指令,指向的符號(hào)引用是 Parent3.sayHi:()V

Java 字節(jié)碼中與調(diào)用相關(guān)的指令共有五種。
invokestatic:用于調(diào)用靜態(tài)方法。
invokespecial:用于調(diào)用私有實(shí)例方法、構(gòu)造器,以及使用 super 關(guān)鍵字調(diào)用父類(lèi)的實(shí)例方法或構(gòu)造器,和所實(shí)現(xiàn)接口的默認(rèn)方法。
invokevirtual:用于調(diào)用非私有實(shí)例方法。
invokeinterface:用于調(diào)用接口方法。
invokedynamic:用于調(diào)用動(dòng)態(tài)方法。

invokevirtual較為復(fù)雜,本文以 invoke_virtual 指令為例,分析 HotSpot JVM 解釋器如何從符號(hào)引用解析出直接引用信息。上文提到,解析的邏輯則是交給了linkResolve這個(gè)類(lèi)來(lái)完成,追一下它的源碼:

void LinkResolver::resolve_virtual_call(CallInfo& result, Handle recv, KlassHandle receiver_klass, KlassHandle resolved_klass,
                                       Symbol* method_name, Symbol* method_signature, KlassHandle current_klass,
                                       bool check_access, bool check_null_and_abstract, TRAPS) {
 methodHandle resolved_method;
 linktime_resolve_virtual_method(resolved_method, resolved_klass, method_name, method_signature, current_klass, check_access, CHECK);
 runtime_resolve_virtual_method(result, resolved_method, resolved_klass, recv, receiver_klass, check_null_and_abstract, CHECK);
}

這段解析邏輯用來(lái)解析下invokevirtual指令對(duì)應(yīng)的符號(hào)引用,在本例中,就是將Method Parent3.sayHi:()V轉(zhuǎn)化為直接引用
可以看到,依次調(diào)用了鏈接時(shí)和運(yùn)行時(shí)的解析方法,以下分別進(jìn)行分析:

連接時(shí)解析方法:

void LinkResolver::linktime_resolve_virtual_method(methodHandle &resolved_method, KlassHandle resolved_klass,
                                                  Symbol* method_name, Symbol* method_signature,
                                                  KlassHandle current_klass, bool check_access, TRAPS) {
 // normal method resolution
 resolve_method(resolved_method, resolved_klass, method_name, method_signature, current_klass, check_access, true, CHECK);

 assert(resolved_method->name() != vmSymbols::object_initializer_name(), "should have been checked in verifier");
 assert(resolved_method->name() != vmSymbols::class_initializer_name (), "should have been checked in verifier");

 // check if private interface method
 if (resolved_klass->is_interface() && resolved_method->is_private()) {
   //拋出異常
 }

 // check if not static
 if (resolved_method->is_static()) {
   ////拋出異常
 }

 //省略
}

除去一些校驗(yàn)邏輯,關(guān)注resolve_method這個(gè)方法

methodHandle LinkResolver::resolve_method(const LinkInfo& link_info,
                                         Bytecodes::Code code, TRAPS) {

 Handle nested_exception;
 KlassHandle resolved_klass = link_info.resolved_klass();

 // 1. For invokevirtual, cannot call an interface method
 ...

 // 2. check constant pool tag for called method - must be JVM_CONSTANT_Methodref
 ...

 // 3. lookup method in resolved klass and its super klasses
 methodHandle resolved_method = lookup_method_in_klasses(link_info, true, false, CHECK_NULL);

 // 4. lookup method in all the interfaces implemented by the resolved klass
 if (resolved_method.is_null() && !resolved_klass->is_array_klass()) { // not found in the class hierarchy
   resolved_method = lookup_method_in_interfaces(link_info, CHECK_NULL);

   if (resolved_method.is_null()) {
     // JSR 292:  see if this is an implicitly generated method MethodHandle.linkToVirtual(*...), etc
     resolved_method = lookup_polymorphic_method(link_info, (Handle*)NULL, (Handle*)NULL, THREAD);
     if (HAS_PENDING_EXCEPTION) {
       nested_exception = Handle(THREAD, PENDING_EXCEPTION);
       CLEAR_PENDING_EXCEPTION;
     }
   }
 }

 // 5. method lookup failed
 ...

 // 6. access checks, access checking may be turned off when calling from within the VM.
 ...

 return resolved_method;
}

即先在本類(lèi)中根據(jù)符號(hào)引用來(lái)找到匹配的方法,如果找不到,就去父類(lèi)中找,還找不到,就去實(shí)現(xiàn)的接口中找(注意,這里的類(lèi)指的是字節(jié)碼中的類(lèi)引用,并未到運(yùn)行時(shí)解析的環(huán)節(jié),即本例中的Parent3,而非Son3),尋找方法的這個(gè)邏輯在在InstanceKlass::find_method_index

int InstanceKlass::find_method_index(const Array<Method*>* methods,
                                    const Symbol* name,
                                    const Symbol* signature,
                                    OverpassLookupMode overpass_mode,
                                    StaticLookupMode static_mode,
                                    PrivateLookupMode private_mode) {
 const bool skipping_overpass = (overpass_mode == skip_overpass);
 const bool skipping_static = (static_mode == skip_static);
 const bool skipping_private = (private_mode == skip_private);
 const int hit = binary_search(methods, name);
 if (hit != -1) {
   const Method* const m = methods->at(hit);

   // Do linear search to find matching signature.  First, quick check
   // for common case, ignoring overpasses if requested.
   if (method_matches(m, signature, skipping_overpass, skipping_static, skipping_private)) {
         return hit;
   }

   // search downwards through overloaded methods
   int i;
   for (i = hit - 1; i >= 0; --i) {
       const Method* const m = methods->at(i);
       assert(m->is_method(), "must be method");
       if (m->name() != name) {
         break;
       }
       if (method_matches(m, signature, skipping_overpass, skipping_static, skipping_private)) {
         return i;
       }
   }
   // search upwards
   for (i = hit + 1; i < methods->length(); ++i) {
       const Method* const m = methods->at(i);
       assert(m->is_method(), "must be method");
       if (m->name() != name) {
         break;
       }
       if (method_matches(m, signature, skipping_overpass, skipping_static, skipping_private)) {
         return i;
       }
   }
   // not found
#ifdef ASSERT
   const int index = (skipping_overpass || skipping_static || skipping_private) ? -1 :
     linear_search(methods, name, signature);
   assert(-1 == index, "binary search should have found entry %d", index);
#endif
 }
 return -1;
}

邏輯比較清晰,就是遍歷類(lèi)的方法列表,根據(jù)符號(hào)引用來(lái)找到匹配的方法,并返回它的直接引用,既然已經(jīng)解析到了方法的直接引用。上面看到運(yùn)行時(shí)的解析方法又是做什么的呢?

public void sayHiTest(){
    Parent3 son3 = new Son3();
    son3.sayHi();
}

本例中,通過(guò)連接時(shí)解析,將方法符號(hào)Method Parent3.sayHi:()V轉(zhuǎn)換為了指向該方法的直接引用,但是,根據(jù)java語(yǔ)義,我們知道,真正執(zhí)行的應(yīng)該是Son3重寫(xiě)的方法,那么,這就是 運(yùn)行時(shí)解析 需要處理的邏輯

運(yùn)行時(shí)解析方法:

void LinkResolver::runtime_resolve_virtual_method(CallInfo& result,
                                                 const methodHandle& resolved_method,
                                                 KlassHandle resolved_klass,
                                                 Handle recv,
                                                 KlassHandle recv_klass,
                                                 bool check_null_and_abstract,
                                                 TRAPS) {

 // setup default return values
 int vtable_index = Method::invalid_vtable_index;
 methodHandle selected_method;

 ...

 // do lookup based on receiver klass using the vtable index
 if (resolved_method->method_holder()->is_interface()) { // default or miranda method
   vtable_index = vtable_index_of_interface_method(resolved_klass,
                          resolved_method);
   assert(vtable_index >= 0 , "we should have valid vtable index at this point");

   selected_method = methodHandle(THREAD, recv_klass->method_at_vtable(vtable_index));
 } else {
   // at this point we are sure that resolved_method is virtual and not
   // a default or miranda method; therefore, it must have a valid vtable index.
   assert(!resolved_method->has_itable_index(), "");
   vtable_index = resolved_method->vtable_index();
   // We could get a negative vtable_index for final methods,
   // because as an optimization they are they are never put in the vtable,
   // unless they override an existing method.
   // If we do get a negative, it means the resolved method is the the selected
   // method, and it can never be changed by an override.
   if (vtable_index == Method::nonvirtual_vtable_index) {
     assert(resolved_method->can_be_statically_bound(), "cannot override this method");
     selected_method = resolved_method;
   } else {
     selected_method = methodHandle(THREAD, recv_klass->method_at_vtable(vtable_index));
   }
 }

 ...

 // setup result
 result.set_virtual(resolved_klass, recv_klass, resolved_method, selected_method, vtable_index, CHECK);
}

尤其注意這行代碼:

selected_method = methodHandle(THREAD, recv_klass->method_at_vtable(vtable_index));

通過(guò)源碼上下文可以看出,jvm可以獲取到調(diào)用invokevirtual時(shí)調(diào)用者的實(shí)際類(lèi)型,即本例中的Son3,本行代碼中recv_klass就是Son3類(lèi)的指向instanceKlass的指針,調(diào)用其method_at_vtable方法即可獲取Son3對(duì)應(yīng)虛方法表中特定索引值的直接引用, 那么jvm如何知道該方法的索引值呢

在連接時(shí)解析階段,就已經(jīng)獲得了父類(lèi)直接引用,讀取其對(duì)應(yīng)的虛表索引值(vtable_index),對(duì)于重寫(xiě)方法,父子類(lèi)的索引值相同,即可直接通method_at_vtable(vtable_index) 方法獲取子類(lèi)方法的直接引用,流程如下:

解析符號(hào)引用.png

至此,java重寫(xiě)、多態(tài)語(yǔ)義在jvm中的大體實(shí)現(xiàn)邏輯已經(jīng)分析完畢,接下來(lái)分析java的另外幾個(gè)語(yǔ)義:重載、隱藏

重載語(yǔ)義

重載相對(duì)于重寫(xiě)而言較為簡(jiǎn)單,其實(shí)現(xiàn)主要在于編譯階段,看下面這個(gè)例子:

public class Parent2 {
 public void sayHi(String string){
   System.out.println("hi,string");
 }

 public void sayHi(Object object){
   System.out.println("hi,object");
 }

 public static void main(String[] args) {
   new Parent2().sayHi("hi");
 }
}

void sayHi(String string)void sayHi(Object object)構(gòu)成重載,看下字節(jié)碼中的區(qū)別:

 public void sayHi(java.lang.String);
   descriptor: (Ljava/lang/String;)V
   flags: ACC_PUBLIC
   ...

 public void sayHi(java.lang.Object);
   descriptor: (Ljava/lang/Object;)V
   flags: ACC_PUBLIC
   ...
 public static void main(java.lang.String[]);
   descriptor: ([Ljava/lang/String;)V
   flags: ACC_PUBLIC, ACC_STATIC
   Code:
     stack=2, locals=1, args_size=1
        0: new           #6                  // class Parent2
        3: dup
        4: invokespecial #7                  // Method "<init>":()V
        7: ldc           #8                  // String hi
        9: invokevirtual #9                  // Method sayHi:(Ljava/lang/String;)V
       12: return

二者的描述符就不相同,由于二者區(qū)分在編譯階段已經(jīng)完成,我們可以認(rèn)為 Java 虛擬機(jī)不存在重載這一概念。因此,在某些文章中,重載也被稱(chēng)為靜態(tài)綁定(static binding),把重寫(xiě)被稱(chēng)為動(dòng)態(tài)綁定(dynamic binding)
繼續(xù)看這個(gè)例子,調(diào)用方法時(shí)傳入的參數(shù)為"hi",既是String類(lèi)型,也是Object類(lèi)型,但是在main()函數(shù)對(duì)應(yīng)的字節(jié)碼中,直接認(rèn)定了調(diào)用(Ljava/lang/String;)V,這是為什么呢

Java 編譯器選取重載方法的過(guò)程共分為三個(gè)階段:

1、在不考慮對(duì)基本類(lèi)型自動(dòng)裝拆箱(auto-boxing,auto-unboxing),以及可變長(zhǎng)參數(shù)的情況下選取重載方法;
2、如果在第 1 個(gè)階段中沒(méi)有找到適配的方法,那么在允許自動(dòng)裝拆箱,但不允許可變長(zhǎng)參數(shù)的情況下選取重載方法;
3、如果在第 2 個(gè)階段中沒(méi)有找到適配的方法,那么在允許自動(dòng)裝拆箱以及可變長(zhǎng)參數(shù)的情況下選取重載方法。
如果 Java 編譯器在同一個(gè)階段中找到了多個(gè)適配的方法,那么它會(huì)在其中選擇一個(gè)最為貼切的,而決定貼切程度的一個(gè)關(guān)鍵就是形式參數(shù)類(lèi)型的繼承關(guān)系。

本例中,String和Object均符合,于是java編譯器選擇了最為貼切的Ljava/lang/String;)V方法

再看隱藏:

隱藏語(yǔ)義

public class Parent4 {
 public static void sayHi(){
   System.out.println("hi,son");
 }
}

class Son4 extends Parent4{
 public static void sayHi(){
     System.out.println("hi,parent");
  }

 public static void main(String[] args) {
   Parent4 son4 = new Son4();
   son4.sayHi();
 }
}

本例中,將打印出"hi,son",這和上面多態(tài)的場(chǎng)景基本一致,區(qū)別在于本例中的方法是靜態(tài)方法,所以盡管真正的實(shí)例是Son4類(lèi)型的,但最后還是調(diào)用的父類(lèi)的方法,jvm是如何處理這個(gè)邏輯的,首先看下字節(jié)碼:

 public static void main(java.lang.String[]);
   descriptor: ([Ljava/lang/String;)V
   flags: ACC_PUBLIC, ACC_STATIC
   Code:
     stack=2, locals=2, args_size=1
        0: new           #5                  // class Son4
        3: dup
        4: invokespecial #6                  // Method "<init>":()V
        7: astore_1
        8: aload_1
        9: pop
       10: invokestatic  #7                  // Method Parent4.sayHi:()V
       13: return

可以看到,方法調(diào)用被編譯成了:

10: invokestatic  #7                  // Method Parent4.sayHi:()V

看看hospot源碼中是如何解析它的:

void LinkResolver::resolve_static_call(CallInfo& result, KlassHandle& resolved_klass, Symbol* method_name,
                                       Symbol* method_signature, KlassHandle current_klass,
                                       bool check_access, bool initialize_class, TRAPS) {
  methodHandle resolved_method;
  linktime_resolve_static_method(resolved_method, resolved_klass, method_name, method_signature, current_klass, check_access, CHECK);
  resolved_klass = KlassHandle(THREAD, resolved_method->method_holder());

  // Initialize klass (this should only happen if everything is ok)
  if (initialize_class && resolved_klass->should_be_initialized()) {
    resolved_klass->initialize(CHECK);
    linktime_resolve_static_method(resolved_method, resolved_klass, method_name, method_signature, current_klass, check_access, CHECK);
  }

  // setup result
  result.set_static(resolved_klass, resolved_method, CHECK);
}

可以看到,與上文中的resolve_virtual_call相比,解析過(guò)程只有連接時(shí)解析(linktime_resolve_static_method),缺少了運(yùn)行時(shí)解析,連接時(shí)解析的邏輯基本一致,而從上文的分析中,連接時(shí)解析 只會(huì)按照字節(jié)碼中的符號(hào)引用來(lái)進(jìn)行解析,自然,本例的sayHi:()方法最后解析出來(lái)的是方法Parent4.sayHi:()V的直接引用,調(diào)用的是Parent4.sayHi()方法,表現(xiàn)出來(lái)的現(xiàn)象就是子類(lèi)的方法隱藏

遮蔽語(yǔ)義

下面這段代碼,在init()函數(shù)中定義了一個(gè)與類(lèi)變量同名的局部變量a

public class Parent5 {
  public String a = "out";
  public void init(){
       String a = "in";
       System.out.println(a);
       System.out.println(this.a);
  }
  public static void main(String[] args){
       new Parent5().init();
  }
}

本例中,a和this.a的值是不同的,當(dāng)使用簡(jiǎn)單名a的時(shí)候,值是“in”,先看看字節(jié)碼:

常量池(部分)

Constant pool:
   #1 = Methodref          #10.#22        // java/lang/Object."<init>":()V
   #2 = String             #23            // out
   #3 = Fieldref           #7.#24         // Parent5.a:Ljava/lang/String;
   #4 = String             #25            // in
   #5 = Fieldref           #26.#27        // java/lang/System.out:Ljava/io/PrintStream;
   #6 = Methodref          #28.#29        // java/io/PrintStream.println:(Ljava/lang/String;)V
   #7 = Class              #30            // Parent5
   #8 = Methodref          #7.#22         // Parent5."<init>":()V
   #9 = Methodref          #7.#31         // Parent5.init:()V
  #10 = Class              #32            // java/lang/Object

init()函數(shù)對(duì)應(yīng)的字節(jié)碼

public void init();
    descriptor: ()V
    flags: ACC_PUBLIC
    Code:
      stack=2, locals=2, args_size=1
         0: ldc           #4                  // String in
         2: astore_1
         3: getstatic     #5                  // Field java/lang/System.out:Ljava/io/PrintStream;
         6: aload_1
         7: invokevirtual #6                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
        10: getstatic     #5                  // Field java/lang/System.out:Ljava/io/PrintStream;
        13: aload_0
        14: getfield      #3                  // Field a:Ljava/lang/String;
        17: invokevirtual #6                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
        20: return

首先看字節(jié)碼中是怎么描述類(lèi)變量a的,

3 = Fieldref           #7.#24         // Parent5.a:Ljava/lang/String;
7 = Class              #30            // Parent5
24 = NameAndType        #11:#12        // a:Ljava/lang/String;
11 = Utf8               a
12 = Utf8               Ljava/lang/String;

常量池中有個(gè)Filedrdf類(lèi)型的符號(hào)引用#3,名稱(chēng)和類(lèi)型在#24常量中存放,拼起來(lái)就是 Parent5.a:Ljava/lang/String;
再看 System.out.println(this.a);這條語(yǔ)句被編譯成了什么:

14: getfield      #3                  // Field a:Ljava/lang/String;
17: invokevirtual #6                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V

即先根據(jù)常量池中的符號(hào)引用#3獲取值(Field a:Ljava/lang/String),然后調(diào)用打印函數(shù),顯然直接指向了類(lèi)變量a
再看局部變量a在字節(jié)碼中如何表示:

#4 = String             #25            // in
#25 = Utf8               in

常量池中通過(guò)這兩個(gè)常量就完成了對(duì)局部變量a的表示,仔細(xì)一看,沒(méi)有變量名,再看System.out.println(a);這條語(yǔ)句是如何讀取變量的:

0: ldc           #4                  // String in
2: astore_1
3: getstatic     #5                  // Field java/lang/System.out:Ljava/io/PrintStream;
6: aload_1
7: invokevirtual #6                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V

直接通過(guò)ldc指令將常量池中的常量#4壓到棧頂,即字符串“in”,astore_1指令將“in”字符串的引用保存在局部變量表中,aload_1指令從局部變量表裝載入“in”字符串的引用到操作數(shù)棧的棧頂,然后執(zhí)行println方法
可以看出,整個(gè)過(guò)程中,并沒(méi)有體現(xiàn)出局部變量a的變量名,自然,盡管在代碼中,類(lèi)變量a和局部變量有著一樣的變量名和類(lèi)型,但是經(jīng)過(guò)編譯后,對(duì)于jvm而言,二者根本不會(huì)產(chǎn)生任何混淆, 甚至如果將局部變量a換成局部變量b,編譯出來(lái)的字節(jié)碼一模一樣
本例就是遮蔽的一種典型場(chǎng)景,可以看出,該語(yǔ)義的實(shí)現(xiàn)在編譯期間就已經(jīng)完成了

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • 第二部分 自動(dòng)內(nèi)存管理機(jī)制 第二章 java內(nèi)存異常與內(nèi)存溢出異常 運(yùn)行數(shù)據(jù)區(qū)域 程序計(jì)數(shù)器:當(dāng)前線程所執(zhí)行的字節(jié)...
    小明oh閱讀 1,279評(píng)論 0 2
  • 《深入理解Java虛擬機(jī)》筆記_第一遍 先取看完這本書(shū)(JVM)后必須掌握的部分。 第一部分 走近 Java 從傳...
    xiaogmail閱讀 5,472評(píng)論 1 34
  • 虛擬機(jī)把描述類(lèi)的數(shù)據(jù)從Class文件加載到內(nèi)存, 并對(duì)數(shù)據(jù)進(jìn)行校驗(yàn)、轉(zhuǎn)換解析和初始化, 最終形成可以被虛擬機(jī)直接使...
    好好學(xué)習(xí)Sun閱讀 1,379評(píng)論 0 3
  • 第6章類(lèi)文件結(jié)構(gòu) 6.1 概述 6.2 無(wú)關(guān)性基石 6.3 Class類(lèi)文件的結(jié)構(gòu) java虛擬機(jī)不和包括java...
    kennethan閱讀 1,070評(píng)論 0 2
  • 要入秋了,衛(wèi)衣成了大勢(shì)。像我這種有時(shí)出門(mén)壓根不想好好搭配,那就擼一件衛(wèi)衣。不會(huì)有人覺(jué)得你穿得邋遢,反而加一點(diǎn)小心思...
    臺(tái)一DDM石晴晴閱讀 200評(píng)論 0 0

友情鏈接更多精彩內(nèi)容