JVM虛擬機(三)類文件結構

代碼編譯的結果是從本地機器碼轉變爲字節碼,是存儲格式發展的一小步,卻是編譯語言發展的一大步。

一、Class類文件結構

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

根據Java虛擬機規範的規定,Class文件歌詩達採用一種類似於C語言結構的僞結構來存儲數據,這種僞結構中只有兩種數據類型:無符號數和表,後面的解析都要以這兩種數據類型爲基礎,所以這裏要先介紹這兩個概念。

無符號數屬於基本的數據類型,以u1,u2,u4,u8來分別表示1字節、2字節、4字節、8字節的無符號數,無符號數可以用來描述數字、索引引用、數量值或者按照UTF-8編碼構成字符串值。

表示由多個無符號數或者其他表作爲數據項構成的複合數據類型,所有表都習慣地以“_info”結尾。表用於描述有層次關係的複合結構的數據,整個Class文件本質上是一張表。


                                                                                  圖 1

無論是無符號數還是表,當需要描述同一類型但數量不定的多個數據時,經常會使用一個前置的容量計數器加若干個連續的數據項的形式,這時稱這一系列連續的某一類型的數據爲某一類型的集合。

在圖1中的數據項,無論是順序還是數量,甚至於數據存儲的字節序(Byte Ordering,Class文件中字節序爲Big-Endian)這樣的細節,都是被嚴格限定的,哪個字節代表什麼含義,長度是多少,先後順序如何,都不允許改變。

1.1魔數與Class文件的版本

每個Class文件的頭4個字節成爲魔數(Magic Number),它的唯一作用是確定這個文件是否爲一個能被虛擬機接受的Class文件。使用魔數而不是擴展名來進行識別主要是基於安全方面的考慮,因爲文件擴展名可以隨意改動。文件格式的制定者可以自由的選擇魔數,只要這個魔數值還沒有被廣泛才用過同時又不會引起混淆即可。Class文件魔數值爲:0xCAFEBABE.

緊接着魔數的4個字節存儲的是Class文件的版本號:第5和第6個字節是次版本號(Minor Version),第7和第8個字節是主版本號(Major Version)。

使用十六進制編輯器WinHex打開.class文件可以清楚的看見開頭的4個字節的十六進制表示的是0xCAFEBABE,代表次版本號的第5和第6個字節值爲0x0000,而主版本號的值爲0x0034,也就是十進制的52。


                                                                                    圖 2

1.2常量池

緊接着主次版本號之後的是常量池的入口,常量池可以理解爲Class文件之中的資源倉庫,它是Class文件結構中與其他項目關聯最多的數據類型,也是佔有Class文件空間最大的數據項目之一,同時它還是在Class文件中第一個出現的表類型數據項目。

由於常量池中的常量數量是不固定的,所以在常量池的入口需要放置一項u2類型的數據,代表常量池容量計數值(constant_pool_count)。這個容器計數是從1開始的。如圖2 所示,常量池容量(偏移地址:0x00000008)爲十六進制數0x0050,即十進制數80,這就代表常量池中有79個常量,索引值範圍爲1~79。在Class文件格式規範制定之時,設計者將第0項常量空出來是有特殊考慮的,這樣做的目的在於滿足後面某些指向常量池索引值多的數據在特定情況下需要表達“不引用任何一個常量池項目”的含,這種情況就可以吧索引值職爲0來表示。

Class文件結構中只有常量池的容量計數從1開始,杜宇其他集合類型,包括接口索引集合、字段表集合、方法表集合等的容量計數都與一般習慣形同,是從0開始。

常量池中只要存放量大類常量:字面量(Literal)和符號引用(Symbolic Reference)。

    字面量比較接近於java語言層面的常量概念,如文本字符串、聲明爲final的常量值等。而符號引用則屬於編譯原理方面的概念,包含了下面三類常量:

    類和接口的全類名(Fully Qualified Name)

    字段的名稱和描述符(Descriptor)

    方法的名稱和描述符

在Class文件中不會保存各種方法、字段的最終內存佈局信息,因此這些字段、方法的符號引用不經過運行期的轉換的話無法得到真正的內存入口地址,也就無法直接被虛擬機使用。當虛擬機運行時,需要從常量池中獲得對應的符號引用,再在類創建時或運行時解析、翻譯到具體的內存地址之中。

常量池中的每一項常量都是一個表,JDK1.7之前共有11種結構各不相同的表結構數據,在JDK1.7中爲了更好第支持動態語言的調用,又額外添加了3種(CONSTANT_MethodHandle_info、CONSTANT_MethodType_info、CONSTANT_InvokeDynamic_info)。

這14種表都有一個共同的特點,就是表開始的第一位是一個u1類型的標誌位(tag,取值見圖3中標誌列),代表當前這個常量屬於哪種常量類型。


                                                                                圖 3

之所以說常量池是最繁瑣的數據,是因爲這14種常量類型各自均有自己的結構。回頭看圖2 中的常量池中的第一項常量,它的標誌位(偏移地址:0x0000000A)是0x07,查圖3標誌列發現這個常量屬於CONSTANT_Class_info類型,次類型的常量代表一個類或者家口的符號引用。

CONSTANT_Class_info的結構見下圖:


                                                                              圖 4

tag是標誌位,用於區分常量類型;name_index是一個索引,指向常量池中一個CONSTANT_Utf8_info類型常量,此常量代表這個類(或接口)的全類名。這裏name_index值(偏移地址:0x0000000B)爲0x0002,即指向了常量池中的第二項常量。在圖2中查找第二項常量,它的標誌位是0x01,查圖3 是CONSTANT_Class_info類型常量,結構是


                                                                            圖 5

length值說明了這個UTF-8編碼的字符串長度是多少字節,它後面緊跟着的長度爲length字節的連續數據是一個使用UTF-8縮略編碼表示的字符串。

UTF-8縮略編碼與普通UTF-8編碼的區別是的:

從‘\u0001’到‘\u007f’之間的字符(相當於1~127的ASCII碼)的縮略碼是一個字節;

從‘\u0080’到‘\u07ff’之間的字符的縮略碼是兩個字節;

從‘\u0800’到‘\uffff’之間的字符的縮略碼是三個字節;

本例中字符串的length值爲0x0023,也就是長35字節,內容是“org/think/java/concurrency/Accessor”。

oracle公司爲我們準備好一個專門的分析Class文件字節碼的工具:javap,使用javap -verbose 參數輸出Accessor.class


                                                                                        圖6



                                                                            圖7

1.3訪問標誌

在常量池之後,緊接着兩個字節代表訪問標誌(access_flags),這個標誌用於識別一些類或接口層次的訪問信息,包括這個Class是類還是接口;是否定義爲public類型;是否定義爲abstract類型;如果是類的話是否聲明爲final等;


                                                                    圖 8


                                                圖 9

access_flags中一共有16個標誌可以使用,當前只使用了8個,沒有使用到的標誌位要求一律爲0。Accessor類是同包類,並且使用了 JDK1.2之後的編譯器進行編譯,因此只有ACC_SUPER標誌爲真。

1.4類索引、父類索引與街樓裏索引集合

類索引(this_class)和父類索引(super_class)都是一個u2類型數據,而接口索引集合(interfaces)是一組u2類型的數據得集合,Class文件中由這三項數據來確定這個類的繼承關係。

類索引用於確定這個類的全限定名,父類索引用於確定這個類的父類的全限定名。由於java語言不支持多重繼承,所以父類索引只有一個。接口索引集合就用來描述這個類實現了哪些接口,這些接口的接口將按照implements後的接口順序從左到右排列在接口索引集合中。

從偏移量0x00000385開始的3個u2類型的值分別爲0x0001、0x0003、0x0001、0x0005,也就是類索引爲1,父類索引爲3,接口索引集合大小爲1,接口索引5,


                                                                        圖 10

1.5字段表集合

字段表(field_info)用於描述接口中或者類中聲明的變量。字段(field)包括類級變量以及實例級變量,但是不包括在方法內部聲明的局部變量。

java描述一個字段包含以下信息:

    字段的作用域(public、private、protected修飾符)、是實例變量還是類變量(static修飾符)、可變性(final)、併發可見性(volatile修飾符,是否強制從主內存讀寫)、可否被序列化(transient修飾符)、字段數據類型(基本類型、對象、數組)、字段名稱。以上信息中各個修飾符都是布爾值,要麼有某個修飾符,要麼沒有,很適合使用標誌位來 表示。而字段叫什麼名字、字段被定義爲什麼數據類型,這些都是無法固定的,只能尹紅常量池中的常量來描述。


                                                                        圖 11

access_flags:字段修飾符;包括以下標誌位:


                                                                               圖 12

接口中的字段必須有ACC_PUBLIC、ACC_STATIC、ACC_FINAL標誌,這是java本身的語言規則規定的。

跟隨access_flags標誌的是兩項索引值:name_index和descriptor_index。它們都是對常量池的引用,分別代表着字段的簡單名稱以及字段和方法的描述符。

全限定名:“org/think/java/concurrency/Accessor”是這個類的全限定名,僅僅把類全名中的“.”替換成了“/”爲了使連續的多個全限定名之間不產生混淆,在使用時最後一般會加入一個“;”。

簡單名稱:沒有類型和參數修飾的方法或者字段名稱,這個類中的run()方法和id字段簡單名稱分別爲“run”和“m”。

描述符的作用是用來描述字段的數據類型、方法的參數列表(包括數量、類型及順序)和返回值。

根據描述符的規則,基本類型參數(byte、char、double、float、int、long、short、boolean)以及代表無返回值得void類型都用一個大寫字母來表示,而對象類型則用字符L加對象全限定名錶示


                                                                                圖 13

對於數組類型,每一唯獨將使用一個前置的“[”字符描述,如“java.lang.String[][]”類型的二維數組,將被記錄爲:“[[Ljava/lang/String;”,一個整形數組“int[]”將被記錄爲“[I”.

描述符描述方法時,按照縣參數列表,後返回值的順序描述,參數列表按照參數的嚴格順序放在一組小括號“()”內。如方法 void Inc()的描述符爲“() V”,方法int indexOf(char[] source,int sourceOffset,int sourceCount,char[] target,int targetOffset,int targetCount,int fromIndex)的描述符爲([CII[CIII) I。

從圖9地址0x0000038D開始,第一個u2類型的數據爲容量計數器fields_counts,其值爲0x0001,說明這個類有一個字段,接下來是access_flags標誌,值爲0x0012,代表public final修飾;name_index值爲0x0007,常量表中第7項是一個“CONSTANT_Utf8_info”類型的字符串,其值爲“id”,代表descriptor_index值爲0x0008,指向常量池字符串“I”。對於本例字段id,它的屬性表計數器爲0,也就是沒有需要額外的信息。如果將字段id聲明改爲 final static int id - 123;那可能會存在一項名稱爲ConstantValue的屬性,其值指向123。

字段表集合中不會列出從超類或者父類接口中繼承而來的字段,但有可能列出原來java代碼之中不存在的字段。譬如在內部類中爲了保持對外部類的訪問性,會自動添加指向外部類實例的字段。

java中字段是無法重載的,兩個字段的數據類型、修飾符不管是否相同,都必須使用不一樣的名稱,但是對於字節碼來講,如果兩個字段的描述符不一致,那字段重命名就是合法的。

1.6方法表集合

方法表結構如同字段表一樣,訪問標誌和屬性表集合的可選項中有所區別:



volatile關鍵字和transient關鍵字不能修飾方法;synchronize、native、strictfp和abstract關鍵字可以修飾方法。

方法裏的Java代碼,經過編譯器編譯成字節碼指令後,存放在方法屬性表集合中的一個叫“Code”的屬性裏面,屬性表作爲Class文件格式中最具擴展性的一種數據項目。

如果父類方法在子類沒有被重寫,方法表集合就不會出現來自父類的方法信息。但同樣的,有可能會出現由編譯器自定添加的方法,最典型的便是類構造器“<clinit>”方法和實例構造器"<init>"方法。

1.7屬性表集合




對於每個屬性,它的名稱都需要從而常量池中引用一個CONSTANT_Utf8_info類型的常量來表示,而屬性值的結構則是完全自定義的,只需要通過一個u4長度的屬性去說明屬性所佔位數即可。

1.7.1Code屬性

java程序方法體中的代碼經過javac編譯器處理後,最終變爲字節碼指令存儲在Code屬性內。如果方法表有Code屬性存在,結構如下:


attribute_name_index是一項指向CONSTANT_Utf8_info型常量的索引,常量值固定爲"Code",它代表了該屬性的屬性名稱;

attribute_length指示了屬性值的長度,由於屬性名稱索引與屬性長度一共爲6字節,所以屬性值的長度固定爲整個屬性表長度減去6個字節。

max_stack代表了操作數棧(OperandStacks)深度的最大值。在方法執行的任意時刻,操作數棧都不會超過這個深度。虛擬機運行的時候需要根據這個值來分配棧幀(StackFrame)中的操作棧深度。

max_locals代表了局部變量表所需的存儲空間。max_locals的單位是Slot,Slot是虛擬機爲局部變量分配內存所使用的最小單位。對於byte、char、float、int、short、boolean和returnAddress等長度不超過32位的數據類型,每個局部變量佔用1個Slot,而double和long這兩種64位的數據類型則需要兩個Slot來存放。方法參數(包括實例方法中的隱藏參數"this")、顯式異常處理器的參數(ExceptionHandlerParameter,就是try-catch語句中catch塊所定義的異常)、方法體中定義的局部變量都需要使用局部變量表來存放。另外,並不是在方法中用到了多少個局部變量,就把這些局部變量所佔Slot之和作爲max_locals的值,原因是局部變量表中的Slot可以重用,當代碼執行超出一個局部變量的作用域時,這個局部變量所佔的Slot可以被其他局部變量所使用,Javac編譯器會根據變量的作用域來分配Slot給各個變量使用,然後計算出max_locals的大小。

code_length和code用來存儲Java源程序編譯後生成的字節碼指令。code_length代表字節碼長度,code是用於存儲字節碼指令的一系列字節流。

如果使用過javap中輸出的“Args_size”的值,可能會有疑問,普通方法(非靜態方法)參數個數爲0時,Args_size=1,這是因爲參數中隱藏了this關鍵字;如果是的靜態方法(static修飾)參數爲0時則,Args_size = 0;

異常表的格式如下圖所示,它包含4個字段,這些字段的含義爲:如果當字節碼在第start_pc行[1]到第end_pc行之間(不含第end_pc行)出現了類型爲catch_type或者其子類的異常(catch_type爲指向一個CONSTANT_Class_info型常量的索引),則轉到第handler_pc行繼續處理。當catch_type的值爲0時,代表任意異常情況都需要轉向到handler_pc處進行處理。


1.7.2 LineNumberTable屬性

LineNumberTable屬性用於描述Java源碼行號與字節碼行號之間的對應關係。



1.7.3 LocalVariableTable屬性

LocalVariableTable屬性用於描述棧幀中局部變量表中的變量與Java源碼中定義的變量之間的關係,它也不是運行時必須的屬性,但默認會生成到Class文件之中,可以在javac中分別使用-g:none 或-g:vars選項來取消或要求生成這項信息。

1.7.4SourceFile屬性

SourceFile屬性用於記錄生成這個Class文件的源碼文件名稱.

1.7.5ConstantValue屬性

ConstantValue屬性的作用是通知虛擬機自動爲靜態變量賦值。只有被static關鍵字修飾的變量纔可以使用這項屬性。對於非static類型的變量(也就是實例變量)的賦值是在實例構造器<init>方法中進行的;而對於類變量,則有兩種方式可以選擇:在類構造器<clinit>方法中或者使用ConstantValue屬性。

目前,javac編譯器的選擇是:如果同時使用final和static來修飾一個變量(按照習慣,這裏稱“常量”更貼切),並且這個變量的數據類型是基本類型或者java.lang.String的話,就生成ConstantValue屬性來進行初始化,如果這個變量沒有被final修飾,或者並非基本類型及字符串,則將會選擇在<clinit>方法中進行初始化。

1.7.6 StackMapTable屬性

這個屬性會在虛擬機類加載的字節碼驗證階段被新類型檢查驗證器(Type Checker)使用。

1.7.7 Signature屬性

任何類、接口、初始化方法或成員的泛型簽名如果包含了類型變量或參數化類型,則Signature屬性會爲它記錄泛型簽名信息。

1.7.8 BootstrapMethods屬性

BootStrapMethods屬性是一個複雜的變長屬性,位於類文件的屬性列表中。這個屬性用於保存invokedynamic指令引用的引導方法限定符。


有關屬性的相關整理還沒完成,待續。。。。。。

參考書籍:《深入理解虛擬機:JVM高級特性與最佳實踐》(第二版)

發佈了14 篇原創文章 · 獲贊 8 · 訪問量 5萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章