1 類檔案資料結構型別
Class檔案結構主要有兩種資料結構:無符號數和表
?無符號數:用來表述數字,索引參考、數量值以及字串等,比如 圖1中型別為u1,u2,u4,u8分別代表1個位元組,2個位元組,4個位元組,8個位元組的無符號數
?表:表是有由多個無符號數以及其它的表組成的復合結構,比如圖1中型別以_info結尾的項為表型別,
2 類結構定義
Class類檔案是緊湊、順序、無空隙的,魔數(MagicNumber)、Class檔案版本(Version)、常量池(Constant_Pool)、訪問標記(Access_flag)、本類(This_class)、父類(Super_class)、介面(Interfaces)、欄位集合(Fields)、方法集合(Methods )、屬性集合(Attributes),其中因為java多繼承所以interfaces介面型別為陣列;attribute_info則是方法表中定義的code索引,指向具體的方法體位元組碼,如圖1所示,
下面用一段程式做說明,此類有介面,有方法、類變數和實體變數,機器是如何識別位元組碼然后按照上面的規則來定義此class類呢?
package com.jd.crm.Logback;
public class TestClass implements Super{
private static final int staticVar = 0;
private int instanceVar=0;
public int instanceMethod(int param) throws Exception{
return param ++;
}
}
interface Super{ }
通過javap幫助決議class檔案格式如下:
Classfile /D:/spm-workspace/test/target/classes/com/jd/crm/Logback/TestClass.class
Last modified 2023-4-14; size 597 bytes
MD5 checksum 9d5dd9fc2145ac17393fee7a707d3b9c
Compiled from "TestClass.java"
public class com.jd.crm.Logback.TestClass implements com.jd.crm.Logback.Super
minor version: 0
major version: 52
flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
#1 = Methodref #4.#26 // java/lang/Object."<init>":()V
#2 = Fieldref #3.#27 // com/jd/crm/Logback/TestClass.instanceVar:I
#3 = Class #28 // com/jd/crm/Logback/TestClass
#4 = Class #29 // java/lang/Object
#5 = Class #30 // com/jd/crm/Logback/Super
#6 = Utf8 staticVar
#7 = Utf8 I
#8 = Utf8 ConstantValue
#9 = Integer 0
#10 = Utf8 instanceVar
#11 = Utf8 <init>
#12 = Utf8 ()V
#13 = Utf8 Code
#14 = Utf8 LineNumberTable
#15 = Utf8 LocalVariableTable
#16 = Utf8 this
#17 = Utf8 Lcom/jd/crm/Logback/TestClass;
#18 = Utf8 instanceMethod
#19 = Utf8 (I)I
#20 = Utf8 param
#21 = Utf8 Exceptions
#22 = Class #31 // java/lang/Exception
#23 = Utf8 MethodParameters
#24 = Utf8 SourceFile
#25 = Utf8 TestClass.java
#26 = NameAndType #11:#12 // "<init>":()V
#27 = NameAndType #10:#7 // instanceVar:I
#28 = Utf8 com/jd/crm/Logback/TestClass
#29 = Utf8 java/lang/Object
#30 = Utf8 com/jd/crm/Logback/Super
#31 = Utf8 java/lang/Exception
{
public com.jd.crm.Logback.TestClass();
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: aload_0
5: iconst_0
6: putfield #2 // Field instanceVar:I
9: return
LineNumberTable:
line 3: 0
line 7: 4
LocalVariableTable:
Start Length Slot Name Signature
0 10 0 this Lcom/jd/crm/Logback/TestClass;
public int instanceMethod(int) throws java.lang.Exception;
descriptor: (I)I
flags: ACC_PUBLIC
Code:
stack=1, locals=2, args_size=2
0: iload_1
1: iinc 1, 1
4: ireturn
LineNumberTable:
line 10: 0
LocalVariableTable:
Start Length Slot Name Signature
0 5 0 this Lcom/jd/crm/Logback/TestClass;
0 5 1 param I
Exceptions:
throws java.lang.Exception
MethodParameters:
Name Flags
param
}
SourceFile: "TestClass.java"
以上是javap幫助我們生成的class檔案決議結果,只是給人看,而非機器,
通過編譯后生成class檔案格式如下,因為class檔案是以8位作為一個位元組的二進制流,為了方便計算,用16進制表示二進制(1個位元組=2個十六進制的數,故下面每2個數就代表1個位元組)
2.1 魔法數
前四個位元組cafebabe是固定值,任何語言編譯成jvm認識的二進制流,前四位必須是固定的cafebabe位元組,
2.2 版本號
緊接著2個位元組00表示次版本號為0 ;0034代表主版本為52(jdk版本號對應的jdk版本為1.8)參考jdk版本和class位元組版本的對應關系
2.3 常量個數
常量個數const_pool_count位元組碼為00 20對應的說明常量個數為32,實際為31個,因為首位jvm作為保留位使用,
2.4 常量池
常量池存放兩大常量:字面量和符號引,字面量如文本字串,被生命的final常量值等,而符號參考則包含類、介面的全限名稱、欄位、方法名稱和描述符號等等,參考javap生成的類檔案資訊,
這里只分析下其中一個常量,在上面常量個數2個位元組后面緊接著一個位元組0a十進制為10,參考常量池型別10代表類中方法的符號參考,繼續參考方法型別MethodRef_info個格式定義:前兩個位元組0004代表方法所在類名稱的索引,后兩個位元組0001a代表一個NameAndType型別的索引,
2.5 類訪問標志
緊接常量池定義完后的u2標識訪問標志,本例標識為0x0021和下圖示志位按位或計算,如0x0001為真,0x0020也為真,其他為否 最終確認訪問標志位ACC_PUBLIC、ACC_SUPER
2.6 本類、父類、介面索引集合
根據圖1的規則,u2兩個位元組0003標識當前類名的參考到,參考常量池陣列下標為#3,根據圖3所示子項的類名為com/jd/crm/Logback/TestClass;0004代表父類類名的參考常量池陣列下標為#4,根據圖4所示參考的父類類名為java/lang/Object;緊接著0001標識介面個數,指明數量為1,0005標識第一個介面陣列中介面的名稱,指向常量池中下標為5的名稱為com/jd/crm/Logback/Super;
比如查找當前類索引如下圖
2.7 欄位表集合
欄位表以陣列的形式定義存盤在常量表中
以上圖說明,0002標識域個數為2個域標識,在本類中有兩個,一個類的域欄位staticVar 一個是實體物件的域欄位instanceVar,如欄位結構定義(下圖)定義,前2個位元組001a為訪問標識,和類訪問標識一樣,分別用001a的二進制和下圖欄位域訪問標識型別做位或運算,得出訪問型別為ACC_PRIVATE型別,name_index的占用兩個位元組0006,指向常量表下標為6的參考,descriptor_index=0007指向常量表下標為7的參考,此處為I標識為資料型別為int,attributes_count=0001為1個,值為0008指向常量表下標為#8的參考常量ConstantValue,標識為靜態變數,最終依次類推第二個域標識參考
欄位結構定義
欄位域的訪問標志請參考類訪問標志,邏輯計算一致,只是規則不一樣而已 如下圖
2.8 方法表集合
和域欄位集合表定義類似 也是陣列方式定義在常量池中 ,其中方法的結構體第四個欄位attributes_count代表方法的屬性數量,attribute_info就是屬性的集合參考屬性表集合
方法表訪問標識型別
通過上面方法的訪問標志、名稱索引和描述索引定義方法的基本資訊,方法的代碼塊則存放于型別為Code的屬性表中,
2.9 屬性表集合
類、欄位表、方法表本身可包含屬性表,屬性表格結構體如下,屬性表結構型別較多,比如有Code型別、Exception型別、MethodParameters型別等等,具體參考屬性表型別,所有的屬性都是參考常量池中的屬性型別名稱,然后根據屬性的長度指定該屬性的內容,根據屬性的不同型別決議不同的屬性值,格式定義如下
以Code屬性舉例,Code屬性結構如下所示
jvm按屬性獲取attribute_name_index指向常量池一個字串常量Code,緊接著attribute_length標識Code型別Info資訊長度,這個info內容包括:max_stack 最大堆疊深,max_locals區域變數槽數量,code_length標識機器位元組碼長度,往后查詢位元組碼如下圖所示,其實就是0/1/4/5/6/9的指令集,Code型別又嵌套例外屬性表、行號表LineNumberTable、LocaVariableTable 區域變數表等等資訊,如下圖javap生成的類定義資訊
1.Code1方法執行程序:
構造方法:descriptor ()V標識無參無回傳值為Void的方法索引,flags可見性修飾符;
程式運行時,先將常量池、方法位元組碼、字串常量池,靜態變數加載到元資料區(1.8后字串常量池,靜態變數放入了堆);main執行緒開始運行,分配堆疊幀記憶體,其中運算元堆疊stack=2表示運行該方法所需要的最大運算元堆疊的深度是2;locals=1表示該運行方法所需要的最大區域方法表的最大slot資料是1;args_size是該方法的形參個數,如果是實體方法 第一個形參是this參考,此例正是this參考,所以args_size=1+實際的引數
aload_0: 加載 slot0的區域變數,即this,作為下面的invokespecial 構造方法呼叫的引數
invokespecial: 呼叫構造方法,常量池第#1項,即【Method java/lang/Object."
aload_0 :再次加載 slot0的區域變數,即this
iconst0: 將int型別為0的數值壓入堆疊頂(為什么要再放入堆疊頂,我個人人為可能是下面初始化實體會需要指定到當前的實體物件)
putfileld: 將常量池中#2 也就是com/jd/crm/Logback/TestClass.instanceVar 實體變數賦值為0,并彈出堆疊,
通過以上指令操作,物件已經初始化,可發現在實體變數初始化之前是先呼叫的構造器方法,后才初始化實體變數,
1.Code2方法instanceMethod執行程序:
descriptor標識為int型別入參、int型別出參
flags標識方法問public型別
statck=2代表堆疊深度為2,locals=2標識預留兩個區域變數槽;args_size=2標識兩個引數,分別為隱藏的this和方法的形式引數,下標[0]=this、 [1]=param 如下所示
LocalVariableTable:
Start Length Slot Name Signature
0 4 0 this Lcom/jd/crm/Logback/TestClass;
0 4 1 param I
0:iload_1 標識將上面區域變數槽LocalVariableTable下標為1的param引數壓入堆疊
1:iconst_1 將int型別為1的常量數字壓入堆疊
2: iadd 將當前堆疊頂的兩個元素 param和1相加
3: ireturn 回傳
LineNumberTable:
line 10: 0
標識實際java源代碼的行數
2.10 位元組碼指令簡介
?加載和存盤指令:
?運算指令
?型別轉換指令
?物件創建和訪問指令
?運算元堆疊管理指令
?控制轉移指令
?例外處理指令
?同步指令
?方法呼叫和回傳執行
invokervirtual:呼叫物件的實體方法 invokerinterface 呼叫介面方法,自動運行期搜索一個實作介面的物件進行方法呼叫;invokerspeical:呼叫init、私有和父類呼叫的特殊方法呼叫;invokedynamic:運行時動態決議
3 類檔案加載
3.1 加載
jvm通過classLoader(雙親委派)將class類檔案二進制流加載到元資料區記憶體,
將位元組流所標識的靜態存盤結構轉換為元資料區的動態存盤
在堆記憶體創建一個Class物件,堆中的Class并不存盤靜態變數、常量、方法等實際資訊(實際存盤元空間),可以看做只是一個句柄,通過物件頭的類指標指向元空間類資訊,這樣在強制轉換或者InstanceOf判斷時,會根據物件中的類指標指向元空間的類常量池進行判斷是否為同一個類,
3.2 驗證
1、檔案格式驗證
2、元資料驗證
3、位元組碼驗證
4、符號參考驗證
3.3 準備
準備階段是為類變數(靜態變數)分配記憶體并設定類變數初始值的階段,分配這些記憶體是在元資料區里面進行的,但是類變數(無final修飾的靜態變數)、字串常量在1.8及以后都放入了堆區間,這個階段有兩點需要重點介紹以下的:
1、只有類變數(被static修飾的變數賦值初始值,static final修飾的賦值為程式指定值)會分配記憶體,不包括實體變數,實體變數是在物件實體化的時候在堆中分配記憶體的,
2、設定類變數的初始值是數量型別對應的默認值,而不是代碼中設定的默認值,例如public static int number=111,這類變數number在準備階段之后的初始值是0而不是111,而給number賦值為111是在類的初始化階段,
3.4 決議
決議階段是虛擬機將常量池內的符號參考替換為直接參考的程序,決議動作主要針對類或介面、欄位、類方法、介面方法、方法型別、方法句柄和呼叫點限定符7類符號參考進行,
符號參考:常量池中類、欄位的常量字串表示方式
類和介面的決議舉例:假如類A參考了類B,加載階段是靜態決議,這時候B還沒有被放到JVM記憶體中,這時候A參考的只是代表B的符號,這是符號參考,
直接參考: 指向目標的指標或者相對偏移量
類和介面的決議舉例:類A在決議階段發現自己符號參考了B,如果這個時候B還沒被加載,就是直接觸發B的類加載,加載后會在運行常量池存盤B的有效類資訊地址,并且直接參考,
?類和介面的決議
?欄位決議根據常量池欄位filedrf_info中的符號進行決議,首先在符號參考的類中根據簡單名稱和欄位描述符查找,如果查到則回傳這個欄位的直接參考并結束,否則從下往上地柜各個父類查找,如果還未查到則拋出NoSuckFieldError例外
?方法決議
?介面方法決議
4 類實體初始化
初始化,為類的靜態變數賦予正確的初始值,JVM負責對類進行初始化,主要對類變數進行初始化clinit方法,在Java中對類變數進行初始值設定有兩種方式:定義靜態變數并指定值、使用靜態代碼塊
物件初始化
4.1 初始化物件前檢查
jvm碰到一個new指令,首先判斷改指令指向的常量池的類全名是否被加載、決議初始化過,如果沒有則進行類加載,參考類檔案加載
4.2 記憶體分配
通過jvm記憶體分配機制,此分配機制取決回識訓制,通過指標碰撞方法或者空閑串列方式進行堆記憶體分配;
1.指標碰撞法 假設Java堆中記憶體是完整的,已分配的記憶體和空閑記憶體分別在不同的一側,通過一個指標作為分界點,需要分配記憶體時,僅僅需要把指標往空閑的一端移動與物件大小相等的距離,使用的GC收集器:Serial、ParNew,適用堆記憶體規整(即沒有記憶體碎片)的情況下,這兩種都是新生代垃圾收集器,因此都是使用復制演算法,可以得到比較完整的記憶體區域,
2.空閑串列法 事實上,Java堆的記憶體并不是完整的,已分配的記憶體和空閑記憶體相互交錯,JVM通過維護一個串列,記錄可用的記憶體塊資訊,當分配操作發生時,從串列中找到一個足夠大的記憶體塊分配給物件實體,并更新串列上的記錄,使用的GC收集器:CMS,適用堆記憶體不規整的情況下,從名字中的Mark Sweep這兩個詞可以看出,CMS 收集器是一種“標記-清除”演算法實作的,因此會得到很多碎片因此和空閑串列配合使用,
記憶體分配并發問題
在創建物件的時候有一個很重要的問題,就是執行緒安全,因為在實際開發程序中,創建物件是很頻繁的事情,作為虛擬機來說,必須要保證執行緒是安全的,通常來講,虛擬機采用兩種方式來保證執行緒安全:
?CAS: CAS 是樂觀鎖的一種實作方式,所謂樂觀鎖就是,每次不加鎖而是假設沒有沖突而去完成某項操作,如果因為沖突失敗就重試,直到成功為止,虛擬機采用 CAS 配上失敗重試的方式保證更新操作的原子性,
?TLAB(本地現成緩沖區): 為每一個執行緒預先分配一塊堆記憶體,JVM在給執行緒中的物件分配記憶體時,首先在TLAB分配,當物件大于TLAB中的剩余記憶體或TLAB的記憶體已用盡時,再采用上述的CAS進行記憶體分配,
4.3 初始化0值
記憶體分配完成后,虛擬機需要將分配到的記憶體空間都初始化為零值(不包括物件頭),這一步操作保證了物件的實體欄位在 Java 代碼中可以不賦初始值就直接使用,程式能訪問到這些欄位的資料型別所對應的零值,
4.4 物件頭設定
初始化零值完成之后,虛擬機要對物件進行必要的設定,例如這個物件是哪個類的實體、如何才能找到類的元資料資訊、物件的哈希碼、物件的 GC 分代年齡等資訊,這些資訊存放在物件頭中,另外,根據虛擬機當前運行狀態的不同,如是否啟用偏向鎖等,物件頭會有不同的設定方式,
4.5 實體構造器初始化
略
4.6 物件的記憶體布局
物件在對中的存盤布局主要分為三部分,物件頭、實體資料、對齊填充
物件頭:
主要兩類:其主要包括兩部分資料:Mark Word、Class物件指標,特別地對于陣列物件而言,其還包括了陣列長度資料,在64位的HotSpot虛擬機下,Mark Word占8個位元組,其記錄了Hash Code、GC資訊、鎖資訊等相關資訊;而Class物件指標則指向該實體的Class物件,
HotSpot物件頭
實體資料:物件定義的實體變數,這部分資料存盤受到虛擬機分配策略引數(-XX:FieldsAllocationStype)和欄位定義的順序影響,HotSpot默認分配的策略是將相同寬度欄位一起存放,父類的變數會出現在子類變數之前,
對齊填充:jvm存盤任何大小必須是8個位元組的整數倍,不夠補齊,這個和類二級制位元組流一致,下面是個無鎖狀態的物件實體化后的資料結構,使用jol工具列印出的實體布局如下
5 物件的訪問
5.1 句柄訪問
Java堆中將會劃分出一塊記憶體來作為句柄池,reference中 存盤的就是物件
的句柄地址,而句柄中包含了物件實體資料與型別資料各自的具體地址信 息
5.2 直接訪問
直接訪問是reference中直接存盤的實體物件的地址,實體物件中包含了類物件的訪問指標,也就是如果訪問類物件需要多一層參考
優缺點
這兩種物件訪問方式各有優勢,使用句柄來訪問的最大好處就是reference中存盤的是穩定的句柄地址,在物件被移動(垃圾收集時移動物件是非常普遍的行為)時只會改變句柄中的實體資料指標,而reference本身不需要修改, 使用直接指標訪問方式的最大好處就是速度更快,它節省了一次指標定位的時間開銷, 由于物件的訪問在Java中非常頻繁,因此這類開銷積少成多后也是一項非常可觀的執行成本,就本書討論的主要虛擬機Sun HotSpot而言,它是使用第二種方式進行物件訪問的,但從整個軟體開發的范圍來看,各種語言和框架使用句柄來訪問的情況也十分常見
6 虛擬機位元組碼執行引擎
6.1 運行時堆疊幀結構
1.區域變數表:在class檔案被編譯時,就已知某個方法的區域變數槽有幾個,主要存放方法引數和方法內部定義的區域變數
2.運算元堆疊:和區域變數表相似,編譯時就明確了運算元堆疊的深度
3.動態鏈接:大部分類在類加載決議程序中,會將符號參考轉為直接參考,也就是在類加載階段清楚呼叫哪個類的哪個方法(這些方法呼叫參考位元組碼指令簡介中invoke*指令),但是有一部分必須在運行期間才能確定目標的方法的直接參考,
4.方法回傳地址
6.2 方法呼叫
1.決議:在內決議階段,會將符號參考轉換為直接參考,這種在決議階段就能確定的呼叫方法版本稱為決議,比如invokesatic invokespecial invokevirtual等等指令指示的方法呼叫
2.靜態分派:方法的多載,虛擬機需要根據方法的入參個數和型別方能定位到某個具體方法,發生在編譯階段,故也屬于一種決議方式
3.多載方法匹配優先級:方法多載程序中,涉及方法的入參和個數,而入參存在自動型別轉換,比如多載方法入參為char型別,如果不存在入參為char型別的方法匹配,則char進行自動型別轉換為int型別,在最終匹配了Int入參型別的方法,方法多載的本質
4.動態分配:如下圖所示,man和women和重新man參考指向women然后方法呼叫sayHello,此時位元組碼顯示的符號參考都是Human#sayHello,但是實際執行結果和指令碼不一致,這是因為invokevirtual指令,在指令呼叫之前都會aload_x來加載實際的資料型別,這就是方法重寫的本質
5.invokedynamic指令:為了解決其他invok*指令方法分配規則完全固化在虛擬機中的問題,jvm支持設計者更高的靈活度,將動態呼叫可以以api的方式直接使用,參考java.lang.invoke包的使用方式,
6.3 基于堆疊的位元組碼解釋執行引擎
jvm是基于堆疊的指令集合,這種指令自身不帶引數,使用運算元堆疊的輸入輸出作為指令本身的引數,物理機一般是基于暫存器的指令集,指令本身攜帶引數并存放在暫存器,
下面是一個基于堆疊來展示在虛擬機中位元組碼是如何執行的,
以上位元組碼執行程序如下
7 容易混淆點
7.1 檔案常量池
類加載后,類的域欄位、方法和類描述資訊會加載到元資料區,既屬于類的靜態常量池
7.2 運行時常量池
我們上面說的class檔案中的常量池,它會在類加載后進入方法區中的運行時常量池,并非只有Class定義的檔案常量合并處理后放入運行時常量池,在運行期間也可以將新的常量放入池中,比如String類的intern方法
7.3 字串常量池
字串常量池存放在堆記憶體(>=1.8)中,堆里邊的字串常量池存放的是字串的參考或者字串(兩者都有),如下圖描述字串創建的堆分布
上圖說明:
參考初始化初始化s、s2是先看常量池,有就回傳物件參考,否則創建abc物件,然后創建s1/s2Ref常量參考回傳
字串相加:先創建StringBuilder物件,然后apend字串a、apend字串b 然后toString(new方法)生成字串ab物件并在字串常量池生成參考回傳,為什么不要字串相加,就是因為會生成大量StringBuilder物件
String s = "a"+"b";//回傳的是常量池的ab字串的參考
String s1 ="ab";
System.out.println(s == s1);//因兩個最終都指向字串常量池,所以為true
new 字串相當于堆創建兩個物件,一個String物件,然后創建字串堆存盤,然后String物件參考到字串的堆存盤,
String s1 ="a";
String s = new String ("a").intern();//強制生成字串常量池參考
System.out.println(s == s1);//回傳true
String s1 ="a";
String s = new String ("a");
System.out.println(s == s1);//回傳false
8 附件
jvm常量池型別和結構體定義
常量池型別
常量池型別結構定義
常見的屬性型別
jdk版本好class位元組版本號對應關系
屬性表型別
作者:京東物流 王北永
來源:京東云開發者社區
轉載請註明出處,本文鏈接:https://www.uj5u.com/houduan/554535.html
標籤:其他
下一篇:返回列表
