java -- JVM的符號引用和直接引用
https://www.zhihu.com/question/50258991
在JVM中類加載過程中,在解析階段,Java虛擬機會把類的二級制數據中的符號引用替換為直接引用。
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文件格式中。
鏈接:https://www.zhihu.com/question/50258991/answer/120450561
來源:知乎
著作權歸作者所有。商業轉載請聯系作者獲得授權,非商業轉載請注明出處。
我了解了調用函數時符號引用如何轉換為直接引用的,但是對於類變量,實例變量的解析方法還是不太清楚。符號引用是只包含語義信息,不涉及具體實現的;而解析(resolve)過后的直接引用則是與具體實現息息相關的。所以當談及某個符號引用被resolve成怎樣的直接引用時,必須要結合某個具體實現來討論才行。
查閱資料后很多人說了一個偏移量的問題,那這個偏移量是相對於什么的偏移量呢?
“相對於什么的偏移量”這就正好是上面說的“實現細節”的一部分了。
例如說,HotSpot VM采用的對象模型,在JDK 6 / 7之間就發生過一次變化。
在對象實例方面,HotSpot VM所采用的對象模型是比較直觀的一種:Java引用通過直接指針(direct pointer)或語義上是直接指針的壓縮指針(compressed pointer)來實現;指針指向的是對象的真實起始位置(沒有在負偏移量上放任何數據)。
對象內的布局是:最前面是對象頭,有兩個VM內部字段:_mark 和 _klass。后面緊跟着就是對象的所有實例字段,緊湊排布,繼承深度越淺的類所聲明的字段越靠前,繼承深度越深的類所聲明的字段越靠后。在同一個類中聲明的字段按字段的類型寬度來重排序,對普通Java類默認的排序是:long/double - 8字節、int/float - 4字節、short/char - 2字節、byte/boolean - 1字節,最后是引用類型字段(4或8字節)。每個字段按照其寬度來對齊;最終對象默認再做一次8字節對齊。在類繼承的邊界上如果有因對齊而帶來的空隙的話,可以把子類的字段拉到空隙里。這種排布方式可以讓原始類型字段最大限度地緊湊排布在一起,減少字段間因為對齊而帶來的空隙;同時又讓引用類型字段盡可能排布在一起,減少OopMap的開銷。
關於對象實例的內存布局,以前我在一個演講里講解過,請參考:http://www.valleytalk.org/wp-content/uploads/2011/05/Java_Program_in_Action_20110727.pdf,第112頁開始。
舉例來說,對於下面的類C,class A { boolean b; Object o1; } class B extends A { int i; long l; Object o2; float f; } class C extends B { boolean b; }
--> +0 [ _mark ] (64-bit header word)
+8 [ _klass ] (32-bit header word, compressed klass pointer)
+12 [ A.b ] (boolean, 1 byte)
+13 [ (padding) ] (padding for alignment, 3 bytes)
+16 [ A.o1 ] (reference, compressed pointer, 4 bytes)
+20 [ B.i ] (int, 4 bytes)
+24 [ B.l ] (long, 8 bytes)
+32 [ B.f ] (float, 4 bytes)
+36 [ B.o2 ] (reference, compressed pointer, 4 bytes)
+40 [ C.b ] (boolean, 1 byte)
+41 [ (padding) ] (padding for object alignment, 7 bytes)
所以C類的對象實例大小,在這個設定下是48字節,其中有10字節是為對齊而浪費掉的padding,12字節是對象頭,剩下的26字節是用戶自己代碼聲明的實例字段。
留意到C類里字段的排布是按照這個順序的:對象頭 - Object聲明的字段(無) - A聲明的字段 - B聲明的字段 - C聲明的字段——按繼承深度從淺到深排布。而每個類里面的字段排布順序則按前面說的規則,按寬度來重排序。同時,如果類繼承邊界上有空隙(例如這里A和B之間其實本來會有一個4字節的空隙,但B里正好聲明了一些不寬於4字節的字段,就可以把第一個不寬於4字節的字段拉到該空隙里,也就是 B.i 的位置)。
同時也請留意到A類和C類都聲明了名字為b的字段。它們之間有什么關系?——沒關系。
Java里,字段是不參與多態的。派生類如果聲明了跟基類同名的字段,則兩個字段在最終的實例中都會存在;派生類的版本只會在名字上遮蓋(shadow / hide)掉基類字段的名字,而不會與基類字段合並或令其消失。上面例子特意演示了一下A.b 與 C.b 同時存在的這個情況。
$ sudo ~/sdk/jdk1.8.0/Contents/Home/bin/java -Xbootclasspath/a:. -jar ~/Downloads/jol-cli-0.5-full.jar internals C
objc[78030]: Class JavaLaunchHelper is implemented in both /Users/krismo/sdk/jdk1.8.0/Contents/Home/bin/java and /Users/krismo/sdk/jdk1.8.0/Contents/Home/jre/lib/libinstrument.dylib. One of the two will be used. Which one is undefined.
# Running 64-bit HotSpot VM.
# Using compressed oop with 3-bit shift.
# Using compressed klass with 3-bit shift.
# Objects are 8 bytes aligned.
# Field sizes by type: 4, 1, 1, 2, 2, 4, 4, 8, 8 [bytes]
# Array element sizes: 4, 1, 1, 2, 2, 4, 4, 8, 8 [bytes]
C object internals:
OFFSET SIZE TYPE DESCRIPTION VALUE
0 4 (object header) 09 00 00 00 (00001001 00000000 00000000 00000000) (9)
4 4 (object header) 00 00 00 00 (00000000 00000000 00000000 00000000) (0)
8 4 (object header) be 3b 01 f8 (10111110 00111011 00000001 11111000) (-134136898)
12 1 boolean A.b false
13 3 (alignment/padding gap) N/A
16 4 Object A.o1 null
20 4 int B.i 0
24 8 long B.l 0
32 4 float B.f 0.0
36 4 Object B.o2 null
40 1 boolean C.b false
41 7 (loss due to the next object alignment)
Instance size: 48 bytes
Space losses: 3 bytes internal + 7 bytes external = 10 bytes total
所以,對一個這樣的對象模型,實例字段的“偏移量”是從對象起始位置開始算的。對於這樣的字節碼:
getfield cp#12 // C.b:Z
這個C.b:Z的符號引用,最終就會被解析(resolve)為+40這樣的偏移量,外加一些VM自己用的元數據。
這個偏移量加上額外元數據比原本的constant pool index要寬,沒辦法放在原本的constant pool里,所以HotSpot VM有另外一個叫做constant pool cache的東西來存放它們。
在HotSpot VM里,上面的字節碼經過解析后,就會變成:
fast_bgetfield cpc#5 // (offset: +40, type: boolean, ...)
(這里用cpc#5來表示constant pool cache的第5項的意思)
於是解析后偏移量信息就記錄在了constant pool cache里,getfield根據解析出來的constant pool cache entry里記錄的類型信息被改寫為對應類型的版本的字節碼fast_bgetfield來避免以后每次都去解析一次,然后fast_bgetfield就可以根據偏移量信息以正確的類型來訪問字段了。
然后說說靜態變量(或者有人喜歡叫“類變量”)的情況。
從JDK 1.3到JDK 6的HotSpot VM,靜態變量保存在類的元數據(InstanceKlass)的末尾。而從JDK 7開始的HotSpot VM,靜態變量則是保存在類的Java鏡像(java.lang.Class實例)的末尾。
Java object InstanceKlass Java mirror
[ _mark ] (java.lang.Class instance)
[ _klass ] --> [ ... ] <-\
[ fields ] [ _java_mirror ] --+> [ _mark ]
[ ... ] | [ _klass ]
| [ fields ]
\ [ klass ]
每個Java對象的對象頭里,_klass字段會指向一個VM內部用來記錄類的元數據用的InstanceKlass對象;InsanceKlass里有個_java_mirror字段,指向該類所對應的Java鏡像——java.lang.Class實例。HotSpot VM會給Class對象注入一個隱藏字段“klass”,用於指回到其對應的InstanceKlass對象。這樣,klass與mirror之間就有雙向引用,可以來回導航。
這個模型里,java.lang.Class實例並不負責記錄真正的類元數據,而只是對VM內部的InstanceKlass對象的一個包裝供Java的反射訪問用。
在JDK 6及之前的HotSpot VM里,靜態字段依附在InstanceKlass對象的末尾;而在JDK 7開始的HotSpot VM里,靜態字段依附在java.lang.Class對象的末尾。
假如有這樣的A類:class A { static int value = 1; }
Java object InstanceKlass Java mirror
[ _mark ] (java.lang.Class instance)
[ _klass ] --> [ ... ] <-\
[ fields ] [ _java_mirror ] --+> [ _mark ]
[ ... ] | [ _klass ]
[ A.value ] | [ fields ]
\ [ klass ]
可以看到這個A.value靜態字段就在InstanceKlass對象的末尾存着了。
這個情況我在前面提到的演講稿的第121頁有畫過一張更好看的圖。
Java object InstanceKlass Java mirror
[ _mark ] (java.lang.Class instance)
[ _klass ] --> [ ... ] <-\
[ fields ] [ _java_mirror ] --+> [ _mark ]
[ ... ] | [ _klass ]
| [ fields ]
\ [ klass ]
[ A.value ]
可以看到這個A.value靜態字段就在java.lang.Class對象的末尾存着了。
所以對於HotSpot VM的對象模型,靜態字段的“偏移量”就是:- JDK 6或之前:相對該類對應的InstanceKlass(實際上是包裝InstanceKlass的klassOopDesc)對象起始位置的偏移量
- JDK 7或之后:相對該類對應的java.lang.Class對象起始位置的偏移量。
其它細節跟實例字段相似,就不贅述了。
===========================================
好奇的同學可能會關心一下上面說的HotSpot VM里的InstanceKlass和java.lang.Class實例都是放哪里的呢?
在JDK 7或之前的HotSpot VM里,InstanceKlass是被包裝在由GC管理的klassOopDesc對象中,存放在GC堆中的所謂Permanent Generation(簡稱PermGen)中。
從JDK 8開始的HotSpot VM則完全移除了PermGen,改為在native memory里存放這些元數據。新的用於存放元數據的內存空間叫做Metaspace,InstanceKlass對象就存在這里。
至於java.lang.Class對象,它們從來都是“普通”Java對象,跟其它Java對象一樣存在普通的Java堆(GC堆的一部分)里。
===========================================
那么如果不是HotSpot VM,而是別的JVM呢?
——什么可能性都存在。總之“偏移量”什么的全看一個具體的JVM實現的內部各種細節是怎樣的。
例如說,一個JVM完全可以把所有類的所有靜態字段都放在一個大數組里,每新加載一個類就從這個數組里分配一塊空間來放該類的靜態字段。那么此時靜態字段“偏移量”可能直接就是這個靜態字段的地址(假定存放它們的數組不移動的話),或者可能是基於這個數組的起始地址的偏移量。
又例如說,一個JVM在實現對象模型時,可能會讓指針不指向對象真正的開頭,而是指向對象中間的某個位置。例如說,還是HotSpot VM那樣的對象布局,指針可以選擇指向很多種地方都合理:(下面還是假定64位HotSpot VM,開壓縮指針)- 指向對象開頭:_mark位於+0,這是HotSpot VM選擇的做法;
- 指向對象頭的第二個字段:_klass位於+0,_mark位於-8。這種做法在某些架構上或許可以加快通過_klass做vtable dispatch的速度,所以也有合理性;
- 指向實際字段的開頭:_mark位於-12,_klass位於-4,第一個字段位於+0。這主要就是覺得字段訪問可能是更頻繁的操作,而潛在可能犧牲一點對象頭訪問的速度。
Maxine VM的對象模型就可以在OHM模型和HOM模型之間選擇。所謂OHM就是Origin-Header-Mixed,也就是指針指向對象頭第一個字段的做法;所謂HOM就是Header-Origin-Mixed,也就是指針指向對象頭之后(也就是第一個字段)的做法。
還有更有趣的對象布局方式:雙向布局(bidirectional layout),例如Sable VM所用的布局。一種典型的方案是把引用類型字段全部放在負偏移上,指針指向對象頭,然后原始類型字段全部放在正偏移量上。這樣的好處是GC在掃描對象的引用類型字段時只需要掃描一塊連續的內存,非常方便。
更多對象布局的例子請跳傳送門:為什么bs虛函數表的地址(int*)(&bs)與虛函數地址(int*)*(int*)(&bs) 不是同一個? - RednaxelaFX 的回答
再例如說,舉個極端的例子:前面的討論都是基於“對象實例里的所有數據分配在一塊連續的內存”的假設上。但顯然這不是唯一的實現方式。一種極端的做法是,對象用鏈表來實現,鏈表上每個節點存放一個字段的值和指向下一個字段的鏈。就像這樣:
typedef union java_value_tag { int32_t int_val; int64_t long_val; /* ... */ object_slot* ref_val; } java_value; typedef struct object_slot_tag { java_value val; struct object_slot_tag* next; } object_slot;
然后假如一個類有3個字段,那么這個類的實例就有4個這樣的object_slot節點組成的鏈表而構成:對象頭 -> 第一個字段 -> 第二個字段 -> 第三個字段 -> NULL。
誰會這么做(掀桌了!其實還真有。有些有趣的實現,為了簡化GC堆的實現,便於減少外部碎片的堆積,而可以把GC堆實現為一個object_slot大數組。這里面由於每個單元的數據都必然一樣大,所以可以有效消除外部碎片——代價則是人為的打碎了一個對象的數據的連續性,增加了內部碎片。
當然做這種取舍的實現非常非常少,所以大家沒怎么見過也是正常… >_<
