1. 程式人生 > >符號引用和直接引用

符號引用和直接引用

先看Class檔案裡的“符號引用”。

考慮這樣一個Java類:
public class X {
  public void foo() {
    bar();
  }

  public void bar() { }
}
它編譯出來的Class檔案的文字表現形式如下:
Classfile /private/tmp/X.class
  Last modified Jun 13, 2015; size 372 bytes
  MD5 checksum 8abb9cbb66266e8bc3f5eeb35c3cc4dd
  Compiled from "X.java"
public class X
  SourceFile: "X.java"
  minor version: 0
  major version: 51
  flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
   #1 = Methodref          #4.#16         //  java/lang/Object."<init>":()V
   #2 = Methodref          #3.#17         //  X.bar:()V
   #3 = Class              #18            //  X
   #4 = Class              #19            //  java/lang/Object
   #5 = Utf8               <init>
   #6 = Utf8               ()V
   #7 = Utf8               Code
   #8 = Utf8               LineNumberTable
   #9 = Utf8               LocalVariableTable
  #10 = Utf8               this
  #11 = Utf8               LX;
  #12 = Utf8               foo
  #13 = Utf8               bar
  #14 = Utf8               SourceFile
  #15 = Utf8               X.java
  #16 = NameAndType        #5:#6          //  "<init>":()V
  #17 = NameAndType        #13:#6         //  bar:()V
  #18 = Utf8               X
  #19 = Utf8               java/lang/Object
{
  public X();
    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
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
               0       5     0  this   LX;

  public void foo();
    flags: ACC_PUBLIC
    Code:
      stack=1, locals=1, args_size=1
         0: aload_0       
         1: invokevirtual #2                  // Method bar:()V
         4: return        
      LineNumberTable:
        line 3: 0
        line 4: 4
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
               0       5     0  this   LX;

  public void bar();
    flags: ACC_PUBLIC
    Code:
      stack=0, locals=1, args_size=1
         0: return        
      LineNumberTable:
        line 6: 0
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
               0       1     0  this   LX;
}
可以看到Class檔案裡有一段叫做“常量池”,裡面儲存的該Class檔案裡的大部分常量的內容。

來考察foo()方法裡的一條位元組碼指令:
1: invokevirtual #2  // Method bar:()V
這在Class檔案中的實際編碼為:
[B6] [00 02]
其中0xB6是invokevirtual指令的操作碼(opcode),後面的0x0002是該指令的運算元(operand),用於指定要呼叫的目標方法。
這個引數是Class檔案裡的常量池的下標。那麼去找下標為2的常量池項,是:
#2 = Methodref          #3.#17         //  X.bar:()V
這在Class檔案中的實際編碼為(以十六進位制表示,Class檔案裡使用高位在前位元組序(big-endian)):
[0A] [00 03] [00 11]
其中0x0A是CONSTANT_Methodref_info的tag,後面的0x0003和0x0011是該常量池項的兩個部分:class_index和name_and_type_index。這兩部分分別都是常量池下標,引用著另外兩個常量池項。
順著這條線索把能傳遞引用到的常量池項都找出來,會看到(按深度優先順序排列):
   #2 = Methodref          #3.#17         //  X.bar:()V
   #3 = Class              #18            //  X
  #18 = Utf8               X
  #17 = NameAndType        #13:#6         //  bar:()V
  #13 = Utf8               bar
   #6 = Utf8               ()V
把引用關係畫成一棵樹的話:
     #2 Methodref X.bar:()V
     /                     \
#3 Class X       #17 NameAndType bar:()V
    |                /             \
#18 Utf8 X    #13 Utf8 bar     #6 Utf8 ()V

標記為Utf8的常量池項在Class檔案中實際為CONSTANT_Utf8_info,是以略微修改過的UTF-8編碼的字串文字。

這樣就清楚了對不對?
由此可以看出,Class檔案中的invokevirtual指令的運算元經過幾層間接之後,最後都是由字串來表示的。這就是Class檔案裡的“符號引用”的實態:帶有型別(tag) / 結構(符號間引用層次)的字串。

==================================================

然後再看JVM裡的“直接引用”的樣子。

這裡就不拿HotSpot VM來舉例了,因為它的實現略複雜。讓我們看個更簡單的實現,Sun的元祖JVM——Sun JDK 1.0.2的32位x86上的做法。
請先參考另一個回答裡講到Sun Classic VM的部分:為什麼bs虛擬函式表的地址(int*)(&bs)與虛擬函式地址(int*)*(int*)(&bs) 不是同一個? - RednaxelaFX 的回答

Sun Classic VM:(以32位Sun JDK 1.0.2在x86上為例)

         HObject             ClassObject
                       -4 [ hdr            ]
--> +0 [ obj     ] --> +0 [ ... fields ... ]
    +4 [ methods ] \
                    \         methodtable            ClassClass
                     > +0  [ classdescriptor ] --> +0 [ ... ]
                       +4  [ vtable[0]       ]      methodblock
                       +8  [ vtable[1]       ] --> +0 [ ... ]
                       ... [ vtable...       ]
(請留心閱讀上面連結裡關於虛方法表與JVM的部分。Sun的元祖JVM也是用虛方法表的喔。)

元祖JVM在做類載入的時候會把Class檔案的各個部分分別解析(parse)為JVM的內部資料結構。例如說類的元資料記錄在ClassClass結構體裡,每個方法的元資料記錄在各自的methodblock結構體裡,等等。
在剛載入好一個類的時候,Class檔案裡的常量池和每個方法的位元組碼(Code屬性)會被基本原樣的拷貝到記憶體裡先放著,也就是說仍然處於使用“符號引用”的狀態;直到真的要被使用到的時候才會被解析(resolve)為直接引用。

假定我們要第一次執行到foo()方法裡呼叫bar()方法的那條invokevirtual指令了。
此時JVM會發現該指令尚未被解析(resolve),所以會先去解析一下。
通過其運算元所記錄的常量池下標0x0002,找到常量池項#2,發現該常量池項也尚未被解析(resolve),於是進一步去解析一下。
通過Methodref所記錄的class_index找到類名,進一步找到被呼叫方法的類的ClassClass結構體;然後通過name_and_type_index找到方法名和方法描述符,到ClassClass結構體上記錄的方法列表裡找到匹配的那個methodblock;最終把找到的methodblock的指標寫回到常量池項#2裡。

也就是說,原本常量池項#2在類載入後的執行時常量池裡的內容跟Class檔案裡的一致,是:
[00 03] [00 11]
(tag被放到了別的地方;小細節:剛載入進來的時候資料仍然是按高位在前位元組序儲存的)
而在解析後,假設找到的methodblock*是0x45762300,那麼常量池項#2的內容會變為:
[00 23 76 45]
(解析後位元組序使用x86原生使用的低位在前位元組序(little-endian),為了後續使用方便)
這樣,以後再查詢到常量池項#2時,裡面就不再是一個符號引用,而是一個能直接找到Java方法元資料的methodblock*了。這裡的methodblock*就是一個“直接引用”

解析好常量池項#2之後回到invokevirtual指令的解析。
回顧一下,在解析前那條指令的內容是:
[B6] [00 02]
而在解析後,這塊程式碼被改寫為:
[D6] [06] [01]
其中opcode部分從invokevirtual改寫為invokevirtual_quick,以表示該指令已經解析完畢。
原本儲存運算元的2位元組空間現在分別存了2個1位元組資訊,第一個是虛方法表的下標(vtable index),第二個是方法的引數個數。這兩項資訊都由前面解析常量池項#2得到的methodblock*讀取而來。
也就是:
invokevirtual_quick vtable_index=6, args_size=1

這裡例子裡,類X對應在JVM裡的虛方法表會是這個樣子的:
[0]: java.lang.Object.hashCode:()I
[1]: java.lang.Object.equals:(Ljava/lang/Object;)Z
[2]: java.lang.Object.clone:()Ljava/lang/Object;
[3]: java.lang.Object.toString:()Ljava/lang/String;
[4]: java.lang.Object.finalize:()V
[5]: X.foo:()V
[6]: X.bar:()V
所以JVM在執行invokevirtual_quick要呼叫X.bar()時,只要順著物件引用查詢到虛方法表,然後從中取出第6項的methodblock*,就可以找到實際應該呼叫的目標然後呼叫過去了。

假如類X還有子類Y,並且Y覆寫了bar()方法,那麼類Y的虛方法表就會像這樣:
[0]: java.lang.Object.hashCode:()I
[1]: java.lang.Object.equals:(Ljava/lang/Object;)Z
[2]: java.lang.Object.clone:()Ljava/lang/Object;
[3]: java.lang.Object.toString:()Ljava/lang/String;
[4]: java.lang.Object.finalize:()V
[5]: X.foo:()V
[6]: Y.bar:()V
於是通過vtable_index=6就可以找到類Y所實現的bar()方法。

所以說在解析/改寫後的invokevirtual_quick指令裡,虛方法表下標(vtable index)也是一個“直接引用”的表現。

關於這種“_quick”指令的設計,可以參考遠古的JVM規範第1版的第9章。這裡有一份拷貝:cs.miami.edu/~burt/refe

在現在的HotSpot VM裡,圍繞常量池、invokevirtual的解析(再次強調是resolve)的具體實現方式跟元祖JVM不一樣,但是大體的思路還是相通的。

HotSpot VM的執行時常量池有ConstantPool和ConstantPoolCache兩部分,有些型別的常量池項會直接在ConstantPool裡解析,另一些會把解析的結果放到ConstantPoolCache裡。以前發過一帖有簡易的圖解例子,可以參考:請問,jvm實現讀取class檔案常量池資訊是怎樣呢?

==================================================

由此可見,符號引用通常是設計字串的——用文字形式來表示引用關係。

而直接引用是JVM(或其它執行時環境)所能直接使用的形式。它既可以表現為直接指標(如上面常量池項#2解析為methodblock*),也可能是其它形式(例如invokevirtual_quick指令裡的vtable index)。
關鍵點不在於形式是否為“直接指標”,而是在於JVM是否能“直接使用”這種形式的資料。