一、前言
代碼編譯的結果從本地機器碼轉變為字節碼,是存儲格式發展的一小步,卻是編程語言發展的一大步。經過多年的發展,目前的計算機仍然只能識別0和1,但是由於近10年內虛擬機以及大量建立在虛擬機之上的程序語言如雨后春筍般出現並蓬勃發展,將我們編寫的程序編譯成二進制本地機器碼(Native Code)已不再是唯一的選擇,越來越多的程序語言選擇了操作系統和機器指令集無關的、平台中立的格式作為程序編譯后的存儲格式。
二、class類文件結構
Class文件是一組以8位字節為基礎單位的二進制流,各項數據嚴格的按照順序緊湊的地排列在Class文件中,中間沒有添加任何分隔符,這使得整個Class文件中存儲的內容幾乎全部是程序運行的必要數據,沒有空隙存在。當遇到需要占用8個字節以上空間的數據項時,則會按照高位在前的方式分割成若干個8位字節進行存儲。
根據Java虛擬機規范的規定,Class文件格式采用一種類似於C語言結構體的偽結構來存儲數據,這種偽結構中只有兩種數據類型L:無符號和表,后面的解析都要以這兩種數據類型為基礎,所以這里先介紹這兩個概念;無符號屬於基本數據類型,以u1,u2,u4,u8來分別代表1個字節,2個字節,4個字節和8個字節的無符號數,無符號數可以用來描述數字、索引引用、數量或者按照utf-8編碼構成字符串值。表是由多個無符號或者其他表作為數據項構成的復合數據類型,所有表都習慣性地以“_info”結尾。表用於描述有層次關系的復合結構的數據,整個Class文件本質上就是一張表,他由一下表格中的數據項構成:
無論是無符號數還是表,當需要描述同一類型但數量不定的多個數據時,經常會使用一個前置的容量計數器加若干個連續的數據項的形式,這是稱這一系列連續的某一類型的數據為某一類型的集合。
三、魔數和Class文件的版本
每個Class文件的頭4個字節稱為魔數(Magic Number),它的唯一作用是確定這個文件是否為一個能被虛擬機接受的Calss文件。很多文件存儲標准中都使用魔數來進行身份識別,使用魔數而不是使用拓展名來進行識別主要是基於安全方面的考慮,因為文件的拓展名可以隨意的改動。Class文集愛你的魔數值為“0xCAFEBABE”,這個魔數值在Java還稱為“Oak”的時候就已經確定下來了。
緊接着摸數的4個字節存儲的是Class文件的版本號:第5和第6個字節是次版本號,第7和第8個字節是主版本號。Java版本號是從45開始的。JDK1.1之后的每一個JDK大版本發布主版本號向上加1,高版本的JDK能向下兼容以前的版本的Class文件,但是不能運行以后的版本的Class文件,即使文件格式並沒有發生任何變化,虛擬機也必須拒絕執行超過其版本號的Class文件。
四、常量池
在主版本和次版本之后的是常量池的入口,由於常量池的中常量數量是不固定的,所以常量池的入口通常需要放置一個常量池容量計數器,計數器是從1開始而不是從0開始,其目的是為了在特殊情況下表達“不引用任何常量池的項目”的情況。
常量池是Class文件中與其他項目關聯最多的數據類型,也是占用Class文件空間最大的數據項目之一。常量池的常量的類型分為:字面量和符號引用。字面量比較接近Java層面的常量的概念,比如文本字符串“abc”,被聲聲明衛final的常量等。符號引用屬於編譯原理的概念,包括以下3個方面:
- 類和接口的全限定名,比如: java.lang.String
- 字段的名稱的描述符,比如private,static等
- 方法的名稱和描述符,比如private,static等描述
常量池中每一個常量都是一個表,在jdk1.7后提供了14種表結構,他們都有一個共同的特點,就是表開始第一個位置是一個u1類型的標志位,代表當前的常量是屬於哪一種類型的。如下表:
五、訪問標志
常量池結束后就是訪問標志(access_flag)了,用於標識一些類或接口的訪問信息,比如這個Class是類還是接口,是public還是private,是否為abstract等,每種訪問信息都是由一個16進制的標志值表示,如果同時表示多種訪問信息,則得到的標志值為這幾種訪問信息的邏輯或,其標志位和含義如下表:
標志名稱 | 標志值 |
含義 |
ACC_PUBLIC | 0X0001 | 是否為public類型 |
ACC_FINAL | 0X0010 | 是否被聲明為final,只有類可以設置 |
ACC_SUPER | 0X0020 | 是否允許使用invokespecial字節碼指令的新語意,invokespecial指令的語意在JDK1.0.2發生過改變,為了區別這條指令使用哪種語意,JDK1.0.2之后編譯 |
ACC_INTERFACE | 0X0200 | 標志這是一個接口 |
ACC_ABSTRACT | 0X0400 | 是否為abstract類型,對於接口或者抽象類來說,此標志值為真,其他類為假 |
ACC_SYNTHETIC | 0X1000 | 標志這個類並非由用戶代碼產生的 |
ACC_ANNOTATION | 0X2000 |
標志這是一個注解 |
ACC_ENUM | 0X4000 | 標志這是一個枚舉 |
六、類索引(this_class)、父類索引(super_class)、接口索引(interfaces)
類索引和父類索引都是一個u2的類型,而接口索引是一個u2類的數據集合,Class中由這三項數據來確定類的繼承關系。類索引、父類索引和接口索引集合都是有序的排列在訪問標識之后,類索引和父類索引兩個u2類型的索引值表示,他們各自指向一個類型為COMNSTANT_Class_info的類描述符常量,通過該常量的索引值找到定義在COMNSTANT_Utf8_info類型的常量中的全限定名字符串,而接口索引集合用來描述這個類實現了哪些接口,這些被實現的接口按implements語句后的接口順序從左往右排列在接口集合中。
七、字段表集合(fileds)
字段表(field_info)用於描述類或者接口中聲明的變量。字段包括了類級別變量和實例變量,但是不包括聲明在方法中的變量。字段的名稱,類型和修飾符等都是無法固定的,只能引用常量池中的常量來描述,可以包括的信息有:
- 字段的作用域,如public,private等修飾符。
- 示例變量還是類變量,如static修飾符。
- 可變性,final修飾符
- 並發可見性,volatile修飾符。
- 可否被序列化,transient修飾符。
- 字段數據類型,基本數據類型,數組,引用類型等。
- 字段名稱
字段表結構如下:
類型 | 名稱 | 數量 |
u2 | access_flags | 1 |
u2 | name_index | 1 |
u2 | descriptor_index | 1 |
u2 | attribute_count | 1 |
attribute_info | attributes | attribute_count |
其中的access_flags與類中的access_flags非常類似,表示數據類型的修飾符,比如public,private,protected等,后面的name_index和descriptor_index都是對常量池的引用,分別表示字段的簡單名稱以及字段和方法的描述符。描述符的作用是用來描述字段的類型,方法的參數列表和返回值,根據描述符的規則,詳細的描述符含義如下:
對於數組類型,每一個維度都將使用一個前置的“[”字符來描述,如一個整數數組int[] 將被記錄為 "[I",二維整數數組int[][] 記錄為 "[[I"。而對於對於一個對象類型比如 String[] 數組,將被記錄為 "[Ljava/lang/String"。用方法描述符描述方法時,先按照方法參數的順序,然后再返回值的順序來描述,比如 int get(String name,int[] index,int i,char c)方法的描述符為 "(Ljava/lang/String[IIC)I"。字段表都包含的固定的數據項在descriptor_index為止,不過在descriptor_index后是一個屬性表集合,用於存儲一些額外的信息。
八、方法表集合(methods)
放發表(method_info)的結構與屬性表的就夠相同,方法里的Java代碼經過編譯器編譯后編程字節碼指令,然后存放在方法屬性表的一個名為“Code”的屬性里,關於屬性表的項目,同樣會在后面跟進行詳細的介紹。
與字段表集合相對應,如果父類方法在子類中沒有被覆蓋,方法表中就不會出現父類的方法的信息,但同樣,有可能會出現會出現由編譯器自動添加的方法,最典型的就是類構造器“<cinit>”方法和實例狗構造器"<init>"方法。
在Java語言中,要重載一個方法,除了要方法與原方法的簡單名稱一樣之外,還必須要求擁有一個與原方法不同的特征簽名,特診簽名就是一個方法中各個參數在常量池中字段符號引用的集合,但是返回值不包含在特征簽名中,因此Java語言中想要覆蓋一個方法的話,如果是返回值不同是無法覆蓋的。
方法表的結構:
方法訪問標志:
九、屬性表集合(attributes)
在Class文件,字段表和方法表中都可以攜帶自己的屬性表集合,用於描述某些場景下專有的信息。屬性表集合沒有那么嚴格的限定,不再要求各個屬性表具有嚴格的順序,並且只要不予已有的屬性表的名字重復,任何人實現的編譯器都可以想屬性表中寫入自己定義的屬性信息,但Java虛擬機在運行時會忽略掉它不認識的屬性。Java虛擬機規范中預定義了9中虛擬機應當被識別的屬性(jdk1.5后又增加了一些新的特性),如下表:
對於每個屬性,它的名稱都需要從常量池中引用的一個CONSTANT_Utf8_info類型的常量來表示,每個屬性值的結構完全可以自定義,只需說明屬性值所需暫用的位數長度即可,一個符合規范的屬性表至少應具備attribute_name_info”、“attribute_length”和至少一項信息屬性。
1)Code屬性
前面已經提到過,Java程序的方法體中的代碼經過編譯器編譯后,生成的字節碼指令會存儲在Code屬性中,但並非所有的方法表都有屬性表,比如抽象類和接口中可能不存在屬性表。屬性表的結構如下如所示:
attribute_name_index是一項指向CONSTANT_Utf8_info型常量的索引,常量固定值為 "Code",它代表了該屬性的名稱。attribute_length表示屬性值的長度,由於屬性名稱索引與屬性長度一共是6個字節,所以屬性值長度為整個屬性表長度減去 6個字節。
max_stack代表操作數棧的最大深度,max_locals代表了局部變量表所需要的空間,它的單位為slot。
code_length和code是用來存儲Java源程序編譯后生成的字節碼指令。code用於存儲字節碼指令的一些列字節流,它是u1類型的單字節,因此取值范圍為0x00到0xFF,那么一共可以存儲256條指令,目前,Java虛擬機規范中已經定義了200條指令。code_length為u4類型,理論上可以達到2^32-1,但是虛擬機中明確的規定了一個方法不允許超過65525條字節碼指令,如果超過了這個數值,編譯器將拒絕編譯。
字節碼指令之后是這個方法顯示處理的異常表集合(exception_table),對於屬性表來說這個屬性不是必須存在的,它的格式如下表所示:
它包含四個字段,這些字段的含義是如果字節從 start_pc 到 end_pc 行之間(不含end_pc)出現了 catch_pc類型或者它的子類類型的異常(catch_type為指向一個CONSTANT_Class_info型常量的索引),則轉到第handler_pc行繼續處理,當catch_pc為0時,代表任何的異常都要轉到handler_pc行進行處理。異常表實際上的Java代碼的一部分,編譯器使用異常表而不是簡單地使用跳轉的命令來實現Java的異常即finally處理機制,也因此,finally里面的代碼內容會在try或catch中的return語句調用之前調用。
2)Exception屬性
這里的Exception屬性的作用是列舉出方法中可能會出現的受檢查異常,也就是方法描述是throws關鍵字后面列舉的異常,它的結構很簡單,只有attribute_name_index、attribute_length、number_of_exceptions、exception_index_table四項。
3)LineNumberTable屬性
它用於描述Java源碼行號與字節碼行號之間的對應關系。
4)LocalVariableTable屬性
它用於描述棧幀中局部變量表中的變量與Java源碼中定義的變量之間的對應關系。
5)SourceFile屬性
它用於記錄生成這個Class文件的源碼文件名稱。
6)ConstantValue屬性
ConstantValue屬性的作用是通知虛擬機自動為靜態變量賦值,只有被static修飾的變量才可以使用這項屬性,在Java中對非static屬性的賦值是在構造器中完成的,而對於類變量,則有兩種方法可以選擇,在類構造器賦值,或者在ConstantValue屬性賦值。
7)InnerClasses屬性
該屬性用於記錄內部類與宿主類之間的關聯。如果一個類中定義了內部類,那么編譯器將會為它及它所包含的內部類生成InnerClasses屬性。
8)Deprecated屬性和Synthetic屬性
該屬性用於表示某個類、字段和方法,已經被程序作者定為不再推薦使用,它可以通過在代碼中使用@Deprecated注釋進行設置。
9)Synthetic屬性
該屬性代表此字段或方法並不是Java源代碼直接生成的,而是由編譯器自行添加的,如this字段和實例構造器、類構造器等。
參考資料: 《深入理解Java虛擬機-JVM高級特性與最佳實踐》 -周志明
Java虛擬機相關系列博客推薦:
喜歡我寫的博客的同學可以關注訂閱號【Java解憂雜貨鋪】,里面不定期發布一些技術干活,也可以免費獲取大量最新最流行的技術教學視頻