1. 程式人生 > >淺析 JVM 中的符號引用與直接引用

淺析 JVM 中的符號引用與直接引用

前言

在 JVM 的學習過程中,一直會遇到符號引用和直接引用這兩個概念。最近我也查閱了一些資料,有了一些初步的認識,記錄在此與大家分享。文中的內容,主要參考自 JVM裡的符號引用如何儲存? 與 自己動手寫Java虛擬機器

關於符號引用與直接引用,我們還是用一個例項來分析吧。看下面的 Java 程式碼:

package test;
public class Test {
    public static void main(String[] args) {
        Sub sub = new Sub();
        int a = 100;
        int d = sub.inc(a);
    }
}
class Sub {
    public int inc(int a) {
        return a + 2;
    }
}

編譯後使用 javap 分析工具,會得到下面的 Class 檔案內容:

Constant pool:
   #1 = Methodref          #6.#15         // java/lang/Object."<init>":()V
   #2 = Class              #16            // test/Sub
   #3 = Methodref          #2.#15         // test/Sub."<init>":()V
   #4 = Methodref          #2.#17         // test/Sub.inc:(I)I
   #5 = Class              #18            // test/Test
   #6 = Class              #19            // java/lang/Object
   #7 = Utf8               <init>
   #8 = Utf8               ()V
   #9 = Utf8               Code
  #10 = Utf8               LineNumberTable
  #11 = Utf8               main
  #12 = Utf8               ([Ljava/lang/String;)V
  #13 = Utf8               SourceFile
  #14 = Utf8               Test.java
  #15 = NameAndType        #7:#8          // "<init>":()V
  #16 = Utf8               test/Sub
  #17 = NameAndType        #20:#21        // inc:(I)I
  #18 = Utf8               test/Test
  #19 = Utf8               java/lang/Object
  #20 = Utf8               inc
  #21 = Utf8               (I)I
{
  public test.Test();
    descriptor: ()V
    Code:
      stack=1, locals=1, args_size=1
         0: aload_0
         1: invokespecial #1                  // Method java/lang/Object."<init>":()V
         4: return

  public static void main(java.lang.String[]);
    descriptor: ([Ljava/lang/String;)V
    Code:
      stack=2, locals=4, args_size=1
         0: new           #2                  // class test/Sub
         3: dup
         4: invokespecial #3                  // Method test/Sub."<init>":()V
         7: astore_1
         8: bipush        100
        10: istore_2
        11: aload_1
        12: iload_2
        13: invokevirtual #4                  // Method test/Sub.inc:(I)I
        16: istore_3
        17: return
}

因為篇幅有限,上面的內容只保留了常量池,和 Code 部分。下面我們主要對 inc 方法的呼叫來進行說明。

符號引用

在 main 方法的位元組碼中,呼叫 inc 方法的指令如下:

13: invokevirtual #4                  // Method test/Sub.inc:(I)I

invokevirtual 指令就是呼叫例項方法的指令,後面的運算元 4 是 Class 檔案中常量池的下標,表示用來指定要呼叫的目標方法。我們再來看常量池在這個位置上的內容:

#4 = Methodref          #2.#17

這是一個 Methodref 型別的資料,我們再來看看虛擬機器規範中對該型別的說明:

CONSTANT_Methodref_info {
    u1 tag;
    u2 class_index;
    u2 name_and_type_index;
}

這實際上就是一種引用型別,tag 表示了常量池資料型別,這裡固定是 10。class_index 表示了類的索引,name_and_type_index 表示了名稱與型別的索引,這兩個也都是常量池的下標。在 javap 的輸出中,已經將對應的關係列印了出來,我們可以直接的觀察到它都引用了哪些型別:

#4 = Methodref          #2.#17         // test/Sub.inc:(I)I
|--#2 = Class              #16            // test/Sub
|  |--#16 = Utf8               test/Sub
|--#17 = NameAndType        #20:#21        // inc:(I)I
|  |--#20 = Utf8               inc
|  |--#21 = Utf8               (I)I

這裡我們將其表現為樹的形式。可以看到,我們可以得到該方法所在的類,以及方法的名稱和描述符。於是我們根據 invokevirtual 的運算元,找到了常量池中方法對應的 Methodref,進而找到了方法所在的類以及方法的名稱和描述符,當然這些內容最終都是字串形式。

實際上這就是一個符號引用的例子,符號引用也可以理解為像這樣使用文字形式來描述引用關係。

直接引用

符號引用在上面說完了,我們知道符號引用大概就是文字形式表示的引用關係。但是在方法的執行中,只有這樣一串字串,有什麼用呢?方法的本體在哪裡?下面這就是直接引用的概念了,這裡我用自己目前的理解總結一下,直接引用就是通過對符號引用進行解析,來獲得真正的函式入口地址,也就是在執行的記憶體區域找到該方法位元組碼的起始位置,從而真正的呼叫方法。

那麼將符號引用解析為直接引用的過程是什麼樣的呢?我這個小渣渣目前也給不出確定的答案,在 JVM裡的符號引用如何儲存? 裡,RednaxelaFX 大大給出了一個 Sun JDK 1.0.2 的實現;在 自己動手寫Java虛擬機器 中,作者給出了一種用 Go 的簡單實現,下面這裡就來看一下這個簡單一些的實現。在 HotSpot VM 中的實現肯定要複雜得多,這裡還是以大致的學習瞭解為主,以後如果有時間有精力,再去研究一下 OpenJDK 中 HotSpot VM 的實現。

不過不管是哪種實現,肯定要先讀取 Class 檔案,然後將其以某種格式儲存在記憶體中,類的資料會記錄在某個結構體內,方法的資料也會記錄在另外的結構體中,然後將結構體之間相互組合、關聯起來。比如,我們用下面的形式來表達 Class 的資料在記憶體中的儲存形式:

type Class struct {
	accessFlags uint16         // 訪問控制
	name string                // 類名
	superClassName string      // 父類名
	interfaceNames []string    // 介面名列表
	constantPool *ConstantPool // 該類對應的常量池
	fields []*Field            // 欄位列表
	methods []*Method          // 方法列表
	loader *ClassLoader        // 載入該類的類載入器
	superClass *Class          // 父類結構體的引用
	interfaces []*Class        // 各個介面結構體的引用
	instanceSlotCount uint     // 類中的例項變數數量
	staticSlotCount uint       // 類中的靜態變數數量
	staticVars Slots           // 類中的靜態變數的引用列表
	initStarted bool           // 類是否被初始化
}

類似的,常量池中的方法引用,也要有類似的結構來表示:

type MethodRef struct {
    cp *ConstantPool  // 常量池
    className string  // 所在的類名
    class *Class      // 所在的類的結構體引用
    name string       // 方法名
    descriptor string // 描述符
    method *Method    // 方法資料的引用
}

回到上面符號解析的例子。當遇到 invokevirtual 指令時,根據後面的運算元,可以去常量池中指定位置取到方法引用的結構體。實際上這個結構體中已經包含了上面看到的各種符號引用,最下面的 method 就是真正的方法資料。類載入到記憶體中時,method 的值為空,當方法第一次呼叫時,會根據符號引用,找到方法的直接引用,並將值賦予 method。從而後面再次呼叫該方法時,只需要返回 method 即可。下面我們看方法的解析過程:

func (self *MethodRef) resolveMethodRef() {
	c := self.ResolvedClass()
	method := lookupMethod(c, self.name, self.descriptor)
	if method == nil {
		panic("java.lang.NoSuchMethodError")
	}
	self.method = method
}

這裡面省略了驗證的部分,包括檢查解析後的方法是否為空、檢查當前類是否可以訪問該方法,等等。首先我們看到,第一步是找到方法對應的類:

func (self *SymRef) ResolvedClass() *Class {
	if self.class == nil {
        d := self.cp.class
        c := d.loader.LoadClass(self.className)
        self.class = c
	}
	return self.class
}

在 MethodRef 結構體中包含對應 class 的引用,如果 class 不為空,則可以直接返回;否則會根據類名,使用當前類的類載入器去嘗試載入這個類。最後將載入好的類引用賦給 MethodRef.class。找到了方法所在的類,下一步就是從類中找到這個方法,也就是方法資料在記憶體中的地址,對應上面的 lookupMethod 方法。查詢時,會遍歷類中的方法列表,這塊在類載入的過程中已經完成,下面是方法資料的結構體:

type Method struct {
	accessFlags uint16
	name string
	descriptor string
	class *Class
	maxStack uint
	maxLocals uint
	code []byte
	argSlotCount uint
}

這個其實就和 Class 檔案中的 Code 屬性類似,這裡面省略了異常和其他的一些資訊。類載入過程中,會將各個方法的 Code 屬性按照上面的結構儲存在記憶體中,然後將類中所有方法的地址列表儲存在 Class 結構體中。當在 Class 結構體中查詢指定方法時,只需要遍歷方法列表,然後比較方法名和描述符即可:

for c := class; c != nil; c = c.superClass {
    for _, method := range c.methods {
        if method.name == name && method.descriptor == descriptor {
        	return method
        }
    }
}

可以看到,查詢方法會從當前方法查詢,如果找不到,會繼續從父類中查詢。除此以外,還會從實現的介面列表中查詢,程式碼中省略了這部分,還有一些判斷的條件。

最終,如果成功找到了指定方法,就會將方法資料的地址賦給 MethodRef.method,後面對該方法的呼叫只需要直接返回 MethodRef.method 即可。

以上便是 自己動手寫Java虛擬機器 一書中,符號引用解析為直接引用的實現。

總結

本文對 JVM 中的符號引用與直接引用的概念,做了一個簡單的介紹。實際上,這只是我在學習過程中,自己對其的一個簡單理解,與實際的 JVM 實現可能相差甚遠,我自己也只是一知半解,似懂非懂,如果以後有時間有精力,會親自看一下 OpenJDK 的原始碼,研究一下 HotSpot VM 的實現。學無止境,與君共勉。