JVM--Class檔案結構
一、程式碼示例
後面的程式碼舉例都已如下程式碼示例
package org.fenixsoft.clazz; public class TestClass { private int m; public int inc() { return m + 1; } }
編譯後的class檔案,使用16進位制文字開啟(我使用的是UItraEdit),內容如下:
二、class檔案結構簡述
class檔案由魔數、版本號(副版本號、主版本號)、常量池資訊(常量池計數器和常量池資料區)、訪問標誌、類索引(當前類索引、父類索引)、介面資料區(介面數量、介面資訊)、方法資料區(方法數量、方法資訊)、屬性資料區(屬性數量、屬性資訊組成)組成。
具體的內容如下表所示,無符號數使用u1、u2、u4、u8分別代表1、2、4、8個位元組,如果有多個無符號組成的表,則以 _info 結尾表示(有符號可以類比為java中的簡單基本資料型別,而有多個無符號組成的表可以類比為java中的一個物件,物件中都是基本資料型別的屬性)
結構如下所示:
三、魔數與版本資訊
1、魔數:由上面的表格可以看到,魔數佔用4個位元組,其值固定為CAFEBABY,與示例中的值一樣
2、副版本號(次版本號):佔用2個位元組,目前沒有使用,為 00 00
3、主版本號:佔用兩個位元組。JAVA的其實版本號為45,即JDK1的版本號為45,每一個大版本號加一;高版本jdk可以執行低版本jdk編譯的class檔案,反之則不可以。以上面的示例程式碼為例,其版本為十六進位制的0x0034,即十進位制的52,那麼編譯class檔案的jdk版本則為jkd8(52-45+1)
四、常量池資料區
(一)常量池儲存資訊
緊跟著主次版本號後面的就是常量池資訊。
常量值中主要存放兩類資訊:字面量和符號引用。
字面量比較接近java中常量的意思,如文字字串、被final修飾的常量等;
而符號引用則屬於編譯原理方面的概念,主要包括:
1、被模組到處或者開放的包
2、類和介面的全限定名
3、欄位的名稱和描述符
4、方法控制代碼和方法型別
5、動態呼叫點和動態常量
最早的常量表中有11種結構不相同的表結構資料,後來為了支援動態語言呼叫,加入了4種,後來又為了支援模組化,又加入了兩種,因此至JDK13,共有17種表結構資料。
每一種表結構資料最少有兩部分組成,tag和info,其中tag是用來標記不同表結構的,info用來表示具體的資訊。具體的info資訊,可以看最後的常量表結構。
(二)int和float常量儲存結構
首先對於對於基本資料型別和String的值對應的info資訊是具體的值,例如int和float的結構如下所示:tag分別為3和4,然後使用4個位元組來表示值。
(三)long和double常量儲存結構
對於long和double型別,tag分別為5和6,並且使用8個位元組儲存其值,其中前4個位元組為高位數,後4個位元組為低位數。
(四)String常量儲存結構
對於String型別的資料,tag為8,info為兩個位元組的字串索引項(也就是下面要說的utf8格式的cp_info)
(五)utf8常量儲存結構
上面String的info資訊即是對一個UTF8格式索引項,那麼utf8格式的常量,tag為1,然後使用兩個位元組儲存utf8編碼位元組陣列的長度,然後使用具體的長度的陣列儲存資料。
對於String儲存具體是怎麼樣的,可以參照下圖,就是定義了一個String,其字串為“JVM原理”,那麼JVM原理是儲存在utf8格式的cp_info中,在utf8的cp_info中,儲存了tag,長度,及具體的字串內容;然後在String的cp_info中,只是儲存了對於utf8的cp_info的索引項。
(六)類儲存結構
而對於類、方法、屬性的cp_info則是由類或介面的索引項和型別名稱的索引項。
以類為例,其tag為7,index為佔用兩個位元組的類名稱索引項,對應的也是一個utf格式的cp_info
常量表詳情:
常量名 | 描述 | 專案(無符號數) | 型別 | 描述 |
CONSTANT_Utf8_info | utf8編碼的字串 | tag | u1 | 值為1 |
length | u2 | utf8的字串佔用了多少位元組數 | ||
bytes | u1 | 長度為length的utf8編碼的字串 | ||
CONSTANT_Integer_info | 整型字面量 | tag | u1 | 值為3 |
bytes | u4 | 按照高位在前儲存的int值 | ||
CONSTANT_Float_info | 浮點數字面量 | tag | u1 | 值為4 |
bytes | u4 | 按照高位在前儲存的float值 | ||
CONSTANT_Long_info | 長整型字面量 | tag | u1 | 值為5 |
bytes | u8 | 按照高位在前儲存的long值 | ||
CONSTANT_Double_info | 雙精度浮點型字面量 | tag | u1 | 值為6 |
bytes | u8 | 按照高位在前儲存的double值 | ||
CONSTANT_Class_info | 類或介面的符號引用 | tag | u1 | 值為7 |
index | u2 | 指向全限定名常量項的索引 | ||
CONSTANT_String_info | 字串型別字面量 | tag | u1 | 值為8 |
index | u2 | 指向字串字面量的索引 | ||
CONSTANT_Fieldref_info | 欄位的符號引用 | tag | u1 | 值為9 |
index | u2 | 指向欄位宣告類或介面描述符CONSTANT_Class_info的索引 | ||
index | u2 | 指向欄位描述符CONSTANT_NameAndType的索引 | ||
CONSTANT_Methodref_info | 類中方法的符號引用 | tag | u1 | 值為10 |
index | u2 | 指向方法宣告類描述符CONSTANT_Class_info的索引。 |
||
index | u2 | 指向名稱及型別描述符CONSTANT_NameAndType的索引 | ||
CONSTANT_InterfaceMethodref_info | 介面中方法的引用 | tag | u1 | 值為11 |
index | u2 | 指向方法宣告介面描述符CONSTANT_Class_info的索引 | ||
index | u2 | 指向名稱及型別描述符CONSTANT_NameAndType的索引 | ||
CONSTANT_NameAndType_info | 欄位或方法的部分符號引用 | tag | u1 | 值為12 |
index | u2 | 指向該欄位或方法名稱常量項的索引 | ||
index | u2 | 指向該欄位或方法描述符常量項的索引 | ||
CONSTANT_MethodHandle_info | 表示方法控制代碼 | tag | u1 | 值為15 |
reference_kind | u1 | 值必須在1-9之間,閉區間,它決定了方法控制代碼的型別,方法控制代碼型別值表示方法控制代碼的位元組碼行為 | ||
reference_index | u2 | 值必須是對常量池的索引 | ||
CONSTANT_MethodType_info | 表示方法型別 | tag | u1 | 值為16 |
descroptor_index | u2 | 值必須是對常量池的有效索引,常量池在該索引處的項必須是CONSTANT_Utf8_info | ||
CONSTANT_Dynamic_info | 表示一個動態計算常量 | tag | u1 | 值為17 |
bootstrap_method_attrindex | u2 | 值必須是當前class檔案中引導方法表的bootstrap_method[]陣列的有效索引 | ||
name_and_type_index | u2 | 值必須是對當前常量池的有效索引,常量池在該索引處的項必須是CONSTANT_NameAndType_info結構,表示方法名和方法描述符 | ||
CONSTANT_InvokeDynamic_info | 表示一個動態方法呼叫點 | tag | u1 | 值為18 |
bootstrap_method_attrindex | u2 | 值必須是當前class檔案中引導方法表的bootstrap_method[]陣列的有效索引 | ||
name_and_type_index | u2 | 值必須是對當前常量池的有效索引,常量池在該索引處的項必須是CONSTANT_NameAndType_info結構,表示方法名和方法描述符 | ||
CONSTANT_Module_info | 表示一個模組 | tag | u1 | 值為19 |
name_index | u2 | 值必須是對當前常量池的有效索引,常量池在該索引處的項必須是CONSTANT_Utf8_info結構,表示模組名稱 | ||
CONSTANT_Package_info | 表示一個模組中開放或者匯出的包 | tag | u1 | 值為20 |
name_index | u2 | 值必須是對當前常量池的有效索引,常量池在該索引處的項必須是CONSTANT_Utf8_info結構,表示模組名稱 |
(七)哪些字面量會進入常量池
結論:
1、final修飾的8種基本資料型別都會進入常量池
2、非final修飾的8種基本資料型別,只有double、long、float的值會進入常量池。
3、常量池種包含的字串型別字面量(雙引號引起來的字串值)
五、訪問標誌和類索引(當前類索引、父類索引)
1、訪問標誌
在常量池結束以後,緊接著兩個位元組表示訪問標誌,這個標誌用於識別一些類或者介面層次的訪問資訊,例如是否為public、是否為abstract、如果是類的話是否被宣告為final等等,具體標誌位及含義如下所示
2、類索引、父類索引、介面集合
類索引和父類索引都是一個u2型別的資料,介面索引集合是一組u2型別的資料集合,class檔案中由這三項資料來確定該型別的繼承關係。類索引用於確定這個類的全限定名,父類索引用於確定這個類父類的全限定名。介面索引集合用來描述這個類實現了哪些介面。
六、欄位表集合和放發表集合
(一)欄位表集合
欄位表集合用於描述介面或者類中宣告的變數。其包含類變數(static修飾)和例項變數。
欄位可以包括的修飾符有欄位的作用域(public、private、protected修飾符)、是例項變數還是類變數(static修飾符)、可變性(final)、併發可見性(volatile修飾符,是否強制從主記憶體讀寫)、可否被序列化(transient修飾符)、欄位資料型別(基本型別、物件、陣列)、欄位名稱。
欄位修飾符放在access_flags專案中,它與類中的access_flags專案是非常類似的,都是一個u2的資料型別。
跟隨access_flags標誌的是兩項索引值:name_index和descriptor_index。它們都是對常量池項的引用,分別代表著欄位的簡單名稱以及欄位和方法的描述符。
基本資料型別(byte、char、double、float、int、long、short、boolean)以及代表無返回值的void型別都用一個大寫字元來表示,而物件型別則用字元L加物件的全限定名來表示
舉例說明
(二)方法表集合
這裡跟上面的欄位表很類似,就不再多說,直接上表
(三)屬性表集合
屬性表(attribute_info),Class檔案、欄位表、方法表都可以攜帶自己的屬性表集合,以描述某些場景專有的資訊。
七、javap命令
可以使用javap命令反編譯class檔案,例如使用javap命令反編譯示例的class檔案:javap -v TestClass
從輸出結果看,首先是警告,然後是檔案地址、最後修改時間、檔案大小、MD5加密、包名、次版本號、主版本號、訪問標誌等內容。
然後就是常量池內容:
1、第一個內容是方法的cp_info,備註裡面可以看到,這個方法名是init方法,其有兩個索引項,方法對應的類或介面和方法的NameAndType,其分別是4和15
先來看4,說明其是一個類,從備註可以看到該類是Object類,其對應18的索引項,指向了Object類;
再來看15,其是方法的NameAndType,分別對應7和8,7說明該方法名稱是init方法,8說明該方法返回值為void
2、第二個內容是欄位的cp_info,存在對應類或介面的索引項和欄位的NameAndType,分別對應3和16
先來看3,其是一個類,有全限定名的索引項,指向了17,再看17,其類為TestClass類。
再來看16,是一個NameAndType的索引項,其中包含了名稱和型別的索引項,對應5和6,5說明了該欄位名稱為m,6說明了該欄位的型別是int
3、再往下就是code區,及code區中方法資訊。
4、然後看具體的init方法,其標識了方法的返回值、訪問標誌、棧深等資訊,然後就是具體的操作,使用aload將1壓進棧,再使用getfield將獲取m,然後使用iconst將m壓進棧,然後使用iadd相加,最後使用ireturn返回結果。