一、前言
程式碼編譯的結果從本地機器碼轉變為位元組碼,是儲存格式發展的一小步,卻是程式語言發展的一大步。經過多年的發展,目前的計算機仍然只能識別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解憂雜貨鋪】,裡面不定期釋出一些技術幹活,也可以免費獲取大量最新最流行的技術教學視訊