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

直接引用和符號引用

而解析階段即是虛擬機器將常量池內的符號引用替換為直接引用的過程。

1.符號引用(Symbolic References):符號引用以一組符號來描述所引用的目標,符號可以是任何形式的字面量,只要使用時能夠無歧義的定位到目標即可。例如,在Class檔案中它以CONSTANT_Class_info、CONSTANT_Fieldref_info、CONSTANT_Methodref_info等型別的常量出現。符號引用與虛擬機器的記憶體佈局無關,引用的目標並不一定載入到記憶體中。在Java中,一個java類將會編譯成一個class檔案。在編譯時,java類並不知道所引用的類的實際地址,因此只能使用符號引用來代替。比如org.simple.People類引用了org.simple.Language類,在編譯時People類並不知道Language類的實際記憶體地址,因此只能使用符號org.simple.Language(假設是這個,當然實際中是由類似於CONSTANT_Class_info的常量來表示的)來表示Language類的地址。各種虛擬機器實現的記憶體佈局可能有所不同,但是它們能接受的符號引用都是一致的,因為符號引用的字面量形式明確定義在Java虛擬機器規範的Class檔案格式中。

2.直接引用:

 直接引用可以是

(1)直接指向目標的指標(比如,指向“型別”【Class物件】、類變數、類方法的直接引用可能是指向方法區的指標)

(2)相對偏移量(比如,指向例項變數、例項方法的直接引用都是偏移量)

(3)一個能間接定位到目標的控制代碼

直接引用是和虛擬機器的佈局相關的,同一個符號引用在不同的虛擬機器例項上翻譯出來的直接引用一般不會相同。如果有了直接引用,那引用的目標必定已經被載入入記憶體中了。

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

考慮這樣一個Java類:

  1. public class X {

  2. public void foo() {

  3. bar();

  4. }

  5. public void bar() { }

  6. }

它編譯出來的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章。這裡有一份拷貝:http://www.cs.miami.edu/~burt/reference/java/language_vm_specification.pdf

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

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

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

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

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

分析二:

符號引用就是字串,這個字串包含足夠的資訊,以供實際使用時可以找到相應的位置。你比如說某個方法的符號引用,如:“java/io/PrintStream.println:(Ljava/lang/String;)V”。裡面有類的資訊,方法名,方法引數等資訊。

當第一次執行時,要根據字串的內容,到該類的方法表中搜索這個方法。執行一次之後,符號引用會被替換為直接引用,下次就不用搜索了。直接引用就是偏移量,通過偏移量虛擬機器可以直接在該類的記憶體區域中找到方法位元組碼的起始位置。

相關推薦

JVM中的直接引用符號引用

JVM在裝載class檔案的時候,會有一步是將符號引用解析為直接引用的過程。 那麼這裡的直接引用到底是什麼呢? 對於指向“型別”【Class物件】、類變數、類方法的直接引用可能是指向方法區的本地指標。 指向例項變數、例項方法的直接引用都是偏移量。例項變數的直接引用可能是

直接引用符號引用

而解析階段即是虛擬機器將常量池內的符號引用替換為直接引用的過程。 1.符號引用(Symbolic References):符號引用以一組符號來描述所引用的目標,符號可以是任何形式的字面量,只要使用時能夠無歧義的定位到目標即可。例如,在Class檔案中它以CONSTANT_C

Java虛擬機器的直接引用符號引用

Java類從載入到虛擬機器記憶體中開始,到卸載出記憶體為止,它的整個生命週期包括,載入 ,驗證 , 準備 , 解析 , 初始化 , 解除安裝 ,總共七個階段。其中驗證 ,準備 , 解析 統稱為連線。 而在解析階段會有一個步將常量池當中二進位制資料當中的符號引用

JVM——直接引用符號引用

在JVM中,類從被載入到虛擬機器記憶體中開始,到卸載出記憶體為止, 它的整個生命週期包括:載入、驗證、準備、解析、初始化、使用和解除安裝7個階段。 而解析階段即是虛擬機器將常量池內的符號引用替換為直接引

Java字面量(Java直接量)符號引用

1、Java字面量(Java直接量) int i = 1;把整數1賦值給int型變數i,整數1就是Java字面量, 同樣,String s = "abc";中的abc也是字面量。 資料型別 直接量描述 舉例 int 整數直接量(可用二、十、八

深入了解java虛擬機---類加載機制主動引用被動引用

沒有 put log 完成 開始 檢查 觸發 清單 場景 當類被編譯為.class文件後,如何在jvm中被加載的呢 總共七個步驟:加載,驗證,準備,解析,初始化,使用,卸載。其中加載,驗證,準備,初始化,卸載都必須按照順序來。解析可以在初始化後再開始。使用就可有可無了

Python中對象的引用共享引用

col 即使 列表 標簽 同一性 例子 垃圾回收 是否 垃圾 在Python中先創建一個對象,然後再將變量指向所創建的對象。 對於每個對象,都有一個頭部信息,在信息中就標記了這個對象的類型信息。每當一個變量名被賦予了一個新的對象,之前那個對象占用的空間就回被回收(如果此時這

Java中的強引用引用

style 關系 term handle ren soft obj jsb false 旭日Follow_24 的CSDN 博客 ,全文地址請點擊: https://blog.csdn.net/xuri24/article/details/81114944 一、強引用

Java基礎篇 - 強引用、弱引用、軟引用引用

splay 查看 tla 之前 for 應用 幹貨 程序 策略 前言 Java執行GC判斷對象是否存活有兩種方式其中一種是引用計數。 引用計數:Java堆中每一個對象都有一個引用計數屬性,引用每新增1次計數加1,引用每釋放1次計數減1。 在JDK 1.2以前的版本中,若

C#系列 ----- 3 值引用物件引用

值型別和引用型別(Value Types Versus Reference Types) 上一篇對於type的定義其實不準確,在此給出更準確的定義。 所有的C#型別包括: Value types Reference types Generic type par

Golang中自動“取引用“解引用”對原值的影響

1. 寫在前面 我們知道Golang在呼叫方法時,會自動對實參進行“取引用”或“解引用”操作。我們在前面的部落格Golang對方法接收者變數的自動“取引用”和“解引用”中也已經討論了容易引起混淆的解/取引用和介面相關的知識,這裡我們將討論另一個問題:“自動取引用”和“自動解引用”會不

Golang對方法接收者變數的自動“取引用“解引用

1. 寫在前面 文章的標題讀起來是有點拗口的,用一個簡單的示例大家便可以一目瞭然了,如下所示,st2會被自動解引用從而呼叫StructTest的printData方法,而st3會被自動取引用從而呼叫StructTest2的printData方法。 但很多時候,我們會發現這種自動的“取

引用引用

強引用是指向記憶體申請一段儲存空間,進行儲存的引用型別的物件的引用,如下建立一個強引用, object obj = new object();obj = 10; 在物件獲得的分配記憶體空間中不僅僅存放了物件的資訊,還存放著該物件被引用的次數。在建立一個強引用時,預設的引用次數為 1,之後每引用一

java軟引用引用

/** * 引用處理 * * @作者 light-zhang * @時間 2018年11月9日 * @product mall-utils * @package cc.zeelan.common.retus * @file CatReference.java * */ public

Android開發優化方案之軟引用引用的使用

物件的引用分為四種級別,為了能更加靈活的控制物件的生命週期。這四種級別由高到低依次為:強引用、軟引用、弱引用和虛引用。 本篇主要介紹軟引用和弱引用的使用和區別 一、軟引用:SoftReference 如果一個物件只具有軟引用,那麼如果記憶體空間足夠,垃圾回收器就不會回收它;如

java中的軟引用引用

在java 中除了基本資料型別之外,其他都是引用資料型別,而引用資料型別又分類四種 強引用 指向通過new得到的記憶體空間的引用叫做強引用,比如 String a = new String(“123”),其中的a就是一個強引用,它指向了一塊記憶體為123的堆空間。平時我們

Java之"強引用、軟引用 引用"

思考:Java中為何會有引用的概念? 思路:在Java裡,當一個物件M被建立的時候,它就被放在heap裡。當GC執行時,如果發現沒有任何引用指向物件M,M就會被回收,用以騰出記憶體空間。 總結:如果一個物件被回收,需要滿足兩個條件: 沒有任何引用指向它 觸發GC(

Excel絕對引用相對引用以及混合引用

$放在行或列前就是絕對行或絕對列,沒有$的行或列就是相對行或列 向下複製公式:列不變行變 向右複製公式:行不變列變 1.相對引用:A2+B2 複製公式時行和列都會隨之變化 A2+B2 向下複製公式複製到n行就是An+Bn ,A2+B2 向右複製公式到G列 就是E2+F2 2.絕對引用:$

Android面試篇之軟引用引用的區別

軟引用所指向的物件要進行回收,需要滿足兩個條件: ● 沒有任何強引用 指向 軟引用指向的物件(記憶體中的Person物件) ● JVM需要記憶體時,即在丟擲OOM之前 即SoftReference變相

引用、弱引用引用處理

前言 之前在Android上使用 Handler 引起了記憶體洩漏。從而認識了弱引用、軟引用、虛引用。今天發現Kotlin 在Android 上Anko庫裡的async, uiThread 裡面居然做了在非同步執行過程中Activity銷燬了uiTh