詳解Class類文件的結構 轉


出處:  詳解Class類文件的結構(上)

     詳解Class類文件的結構(下)

 

前言

  相信搞Java開發的同學都經常會接觸到Class類文件,了解了JVM虛擬機之后也會大量接觸到class字節碼,那么它到底是什么樣的文件?內部由什么構成?虛擬機又是如何去識別它的?這篇文章就來學習一下Class類文件的結構。

ps:我在面試螞蟻的時候被問到過這個問題!你沒看錯,面試也有可能會問。

一、什么是Class文件

  Class文件又稱字節碼文件,一種二進制文件,它是由某種語言經過編譯而來,注意這里並不一定是Java語言,還有可能是Clojure、Groovy、JRuby、Jython、Scala等,Class文件運行在Java虛擬機上。Java虛擬機不與任何一種語言綁定,它只與Class文件這種特定的二進制文件格式所關聯。

  虛擬機具有語言無關性,它不關心Class文件的來源是何種語言,它只關心Class文件中的內容。Java語言中的各種變量、關鍵字和運算符號的語義最終都是由多條字節碼命名組合而成的,因此字節碼命令所能提供的語義描述能力比Java語言本身更加強大。

二、Class文件的結構

  虛擬機可以接受任何語言編譯而成的Class文件,因此也給虛擬機帶來了安全隱患,為了提供語言無關性的功能就必須做好安全防備措施,避免危險有害的類文件載入到虛擬機中,對虛擬機造成損害。所以在類加載的第二大階段就是驗證,這一步工作是虛擬機安全防護的關鍵所在,其中檢查的步驟就是對class文件按照《Java虛擬機規范》規定的內容來對其進行驗證。

1.總體結構

  Class文件是一組以8位字節為基礎單位的二進制流,各個數據項目嚴格按照順序緊湊地排列在Class文件之中,中間沒有添加任何分隔符,Class文件中存儲的內容幾乎全部是程序運行的必要數據,沒有空隙存在。當遇到需要占用8位字節以上空間的數據項時,就按照高位在前的方式分割成若干個8位字節進行存儲。

  Class文件格式采用類似於C語言結構體的偽結構來存儲數據,這種偽結構只有兩種數據類型:無符號數和表。

  • 無符號數屬於基本的數據類型,以u1、u2、u4、u8來分別代表1個字節、2個字節、4個字節、8個字節的無符號數,無符號數可以來描述數字、索引引用、數量值或者按照UTF-8編碼構成字符串值。
  • 表是由多個無符號數或者其他表作為數據項構成的復合數據類型,所有表都習慣性的以“_info”結尾。表用於描述有層次關系的復合結構的數據,整個Class文件本質上就是一張表,它的數據項構成如下圖。

2.魔數(Magic Number)

  每一個Class文件的頭4個字節成為魔數(Magic Number),它的唯一作用是確定這個文件是否是一個能被虛擬機接收的Class文件。很多文件存儲標准中都是用魔數來進行身份識別,比如gif、png、jpeg等都有魔數。使用魔數主要是來識別文件的格式,相比於通過文件后綴名識別,這種方式准確性更高,因為文件后綴名可以隨便更改,但更改二進制文件內容的卻很少。Class類文件的魔數是Oxcafebabe,cafe babe?咖啡寶貝?至於為什么是這個, 這個名字在java語言誕生之初就已經確定了,它象征着著名咖啡品牌Peet's Coffee中深受歡迎的Baristas咖啡,Java的商標logo也源於此。

3.文件版本(Version)

  在魔數后面的4個字節就是Class文件的版本號,第5和第6個字節是次版本號(Minor Version),第7和第8個字節是主版本號(Major Version)。Java的版本號是從45開始的,JDK1.1之后的每個JDK大版本發布主版本號向上加1(JDK1.0~1.1使用的版本號是45.0~45.3),比如我這里是十六進制的Ox0034,也就是十進制的52,所以說明該class文件可以被JDK1.8及以上的虛擬機執行,否則低版本虛擬機執行會報java.lang.UnsupportedClassVersionError錯誤。

4.常量池(Constant Pool)

  在主版本號緊接着的就是常量池的入口,它是Class文件結構中與其他項目關聯最多的數據類型,也是占用空間最大的數據之一。常量池的容量由后2個字節指定,比如這里我的是Ox001d,即十進制的29,這就表示常量池中有29項常量,而常量池的索引是從1開始的,這一點需要特殊記憶,因為程序員習慣性的計數法是從0開始的,而這里不一樣,所以我這里常量池的索引范圍是1~29。設計者將第0項常量空出來是有目的的,這樣可以滿足后面某些指向常量池的索引值的數據在特定情況下需要表達“不引用任何一個常量池項目”的含義。

  通過javap -v命令反編譯出class文件之后,我們可以看到常量池的內容:

常量池中主要存放兩大類常量:字面量符號引用。比如文本字符、聲明為final的常量值就屬於字面量,而符號引用則包含下面三類常量:

  • 類和接口的全限名
  • 字段的名稱和描述符
  • 方法的名稱和描述符

  在之前的文章(詳談類加載的全過程)中有詳細講到,在加載類過程的第二大階段連接的第三個階段解析的時候,會將常量池中的符號引用替換為直接引用。相信很多人在開始了解那里的時候也是一頭霧水,作者我也是,當我了解到常量池的構成的時候才明白真正意思。Java代碼在編譯的時候,是在虛擬機加載Class文件的時候才會動態鏈接,也就是說Class文件中不會保存各個方法、字段的最終內存布局信息,因此這些字段、方法的符號引用不經過運行期轉換的話無法獲得真正的內存入口地址,也就無法直接被虛擬機使用。當虛擬機運行時,需要從常量池獲得對應的符號引用,再在類創建時或運行時解析、翻譯到具體的內存地址之中

  常量池中每一項常量都是一張表,這里我只找到了JDK1.7之前的常量池項目類型表,見下圖。

  • 常量池項目類型表:
  • 常量池常量項的結構總表:

  比如我這里測試的class文件第一項常量,它的標志位是Ox0a,即十進制10,即表示tag為10的常量項,查表發現是CONSTANT_Methodref_info類型,和上面反編譯之后的到的第一個常量是一致的,Methodref表示類中方法的符號引用。查上面《常量池常量項的結構總表》可以看到Methodref中含有3個項目,第一個tag就是上述的Ox0a,那么第二個項目就是Ox0006,第三個項目就是Ox000f,分別指向的CONSTANT_Class_info索引項和CONSTANT_NameAndType_info索引項為6和15,那么反編譯的結果該項常量指向的應該是#6和#15,查看上面反編譯的圖應證我們的推測是對的。后面的常量項就以此類推。

  這里需要特殊說明一下utf8常量項的內容,這里我以第29項常量項解釋,也就是最后一項常量項。查《常量池常量項的結構總表》可以看到utf8項有三個內容:tag、length、bytes。tag表示常量項類型,這里是Ox01,表示是CONSTANT_Utf8_info類型,緊接着的是長度length,這里是Ox0015,即十進制21,那么再緊接着的21個字節都表示該項常量項的具體內容。特別注意length表示的最大值是65535,所以Java程序中僅能接收小於等於64KB英文字符的變量和變量名,否則將無法編譯

5.訪問標志(Access Flags)

  在常量池結束后,緊接着的兩個字節代表訪問標志(Access Flags),該標志用於識別一些類或者接口層次的訪問信息,其中包括:Class是類還是接口、是否定義為public、是否定義為abstract類型、類是否被聲明為final等。

訪問標志表

  標志位一共有16個,但是並不是所有的都用到,上表只列舉了其中8個,沒有使用的標志位統統置為0,access_flags只有2個字節表示,但是有這么多標志位怎么計算而來的呢?它是由標志位為true的標志位值取或運算而來,比如這里我演示的class文件是一個類並且是public的,所以對應的ACC_PUBLIC和ACC_SIPER標志應該置為true,其余標志不滿足則為false,那么access_flags的計算過程就是:Ox0001 | Ox0020 = Ox0021

 

Test.class文件,它是由下面單獨的一個類文件編譯而成的,沒有包。

6. 索引(Index)

  索引又分類索引、父類索引和接口索引集合,類索引(this_class)和父類索引(super_class)都是一個u2類型的數據,而接口索引集合(interfaces)是一組u2類型的數據的集合,Class文件依靠這些索引數據來確定這個類的繼承關系。所有類(除了java.lang.Object)都只有一個父類索引(Java的單繼承),即父類索引不為0,只有java.lang.Object的父類索引為0。接口索引用來描述該類實現了哪些接口,它們的出現順序是按照implements語句后接口的先后順序出現的,如果這個類是一個接口就按照extends后面出現的順序來。

  類索引和父類索引各自指向一個CONSTANT_Class_info的類描述符常量,然后通過CONSTANT_Class_info可以定位到一個CONSTANT_Utf8_info類型的常量中的全限名字符串。而接口索引集合則以接口計數器開頭,和前面常量池類似,若計數器表示n則后面緊跟着的n個u2數據是表示該類實現的n個接口的類索引,分別指向對應的類描述符常量。

全限名:"java/lang/Object"表示Object類的全限名,將類全名中的“.”替換成“/”而已,多個全限名之間是“;”分隔。

  仍然以我上次的那個Test.class文件為例,這里三個u2類型的值分別為Ox0005、Ox0006、Ox0000,前兩個分別表示的是類索引、父類索引所指向的常量描述符。第三個表示接口集合的個數,這里為0即沒有實現任何接口。假設為2,則表示接下來的2個u2數據表示實現的兩個接口,每個u2數據也指向的是常量描述符。

7.字段表集合(Field Info)

  字段表(field_info)用於描述接口或者類中聲明的變量。字段包括類級變量以及實例級變量,但不包括在方法內部聲明的局部變量。字段包含的信息比較多,包含以下內容:

  • 字段的作用域:public、private、protect修飾符
  • 變量類型(類變量or實例變量):static
  • 可變性:final
  • 並發可見性:volatile
  • 可否序列化:transient
  • 數據類型:基本數據類型、對象、數組
  • 字段名稱

  上面的這些信息除了字段數據類型和字段名稱其他都是以布爾值來描述的,有就是true且對應一個標志位,沒有則false,這種表示方法和上一節的Access Flags一樣。字段數據類型和字段名稱是引用的常量池中的常量來描述,可能是CONSTANT_Class_info也可能是CONSTANT_Utf8_info。

  根據Java語言的語法我們可以知道,ACC_PUBLIC、ACC_PRIVATE、ACC_PROTECTED三個標志只能選一個,ACC_FINAL、ACC_VOLATILE不能同時存在,接口必須有ACC_PUBLIC、ACC_STATIC、ACC_FINAL標志。

描述符

  描述符的作用是用來描述字段的數據類型、方法的參數列表(數量、類型、順序)和返回值。其中基本數據類型以及void返回值類型都是用一個大寫字母來表示的,對象的類型由一個L加對象全限名表示。

  基本數據類型和普通類型都已經知道怎么表示了,但Java中有一個特殊類型就是數組類型,它是在編譯期產生的,它的描述符是在變量描述符前面加一個"[",如果是二維則加兩個[,比如"[["。例如一個String[][]記錄為[[Ljava/lang/String,一個int[]記錄為[I

  如果是描述一個方法則在描述符前面加一個括號“()”,如果有參數則在其中按順序添加描述符即可。例如一個String toString(char[] c,int a,String[] b)的描述符為:“([CI[Ljava.lang.String)Ljava.lang.String”。

  這里同樣以Test.class文件來驗證,第一個u2數據是容量技術器fields_count,這里是Ox0000,說明沒有字段表數據,看文章開頭的java代碼,確實沒有定義任何字段。由於在編譯class文件開始沒有考慮周全,沒有定義字段,這里容量技術器為0也就看不到后面的字段描述內容,這里先假設是Ox0001,即有一個字段。第二個u2數據是訪問標識符access_flags,假設這里是Ox0002,說明字段標志為ACC_PRIVATE。第三個u2數據是字段名稱name_index,假設值為Ox0005,指向#5的常量池CONSTANT_Utf8_info字符串。第四個u2數據是字段描述符,這里是Ox0007,指向#7的常量池字符串。

8. 方法表集合

  方法表的描述和字段表集合描述形式一樣,只需要按照對應的表格對照就可以了。方法表結構依次包含了access_flags(訪問標志)、name_index(方法名索引)、descriptor_index(描述符索引)、attribute(屬性表集合)幾項。方法內的具體代碼存放在屬性表集合attribute的名為“Code”的屬性里面。

方法表結構表:

方法訪問標志表:

  繼續以Test.class文件分析,容量計數器methods_count的值為Ox0002,表示由兩個方法,疑惑?看文章開頭的代碼只有一個main方法啊,為什么會有兩個?其實字節碼中包含了平時省略了的無參構造方法<init>。

  緊跟着的是2個方法描述集合,這里以第一個無參構造來解釋,首先是訪問標志access_flags,值是Ox0001,查表可知是ACC_PUBLIC類型的,然后是方法名索引name_index,值是Ox0007,指向的是常量池CONSTANT_Utf8_info字符串,即#7,我們查看反編譯的代碼可以看到#7確實是<init>。

  然后是描述符索引descriptor_index,值是Ox0008指向的是常量項#8,反編譯后看到是()V,構造方法無返回值,所以用的void的標識字符V,但是在書寫代碼時不能顯式加void,因為其驗證是在編譯期。緊接着的是屬性表集合的屬性計數量attributes_count,這里是Ox0001,說明只有一個屬性,即前面說的“Code”屬性。

  接下來的就是分別表示每一個屬性的具體指向,這里只有一個當然就只需看一個u2數據,這里是Ox0009,指向的是常量項#9,反編譯結果#9確實是Code。

如果方法在子類中沒有被重寫,方法表集合中就不會出現來自父類的信息。

  從方法表集合可以看出,Class文件對一個方法的特征識別(《Java虛擬機規范》稱之為特征簽名)有很多,比如方法描述符、訪問控制標志、返回值、屬性表等。這里我想起來了之前騰訊一個面試官問我的問題“重載的驗證是在哪個階段?”,當時我沒回答好這個問題,看了《深入理解Java虛擬機》這一節的內容才知道,對於Java方法的重載是在編譯器驗證的,在Java語義里規定:只要方法名、參數內容及順序相同則視為非法重載,而對返回值、修飾符等沒有嚴格要求。而在Class文件里對一個方法的特征簽名比編譯期的多,也就是說如果兩個方法有相同的名稱和特征簽名,但返回值不同,那么也是可以合法存在於同一個Class文件的。

9.屬性表集合

  屬性表(attribute_info)存在於Class文件、字段表、方法表等,它用於描述某些場合專有的信息。在class文件中對屬性表的限定並不是很嚴格,只要不要與已有屬性名重復,任何不人實現的編譯器都可以向屬性表中寫入自己定義的屬性信息,虛擬機在運行時會忽略掉它不認識的屬性。這一部分內容較多並且不固定,建議讀者閱讀最新的《Java虛擬機規范》或《深入理解Java虛擬機——周志明 著》。

  本文是筆者閱讀《深入理解Java虛擬機》一書時的簡單總結和實踐。參考文獻:《Java虛擬機規范(第二版)》、《深入理解Java虛擬機》

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM