代碼編譯的結果從本地機器碼轉變爲字節碼,是存儲格式發展的一小步,倒是編程語言發展的一大步。通過多年的發展,目前的計算機仍然只能識別0和1,可是因爲近10年內虛擬機以及大量創建在虛擬機之上的程序語言如雨後春筍般出現並蓬勃發展,將咱們編寫的程序編譯成二進制本地機器碼(Native Code)已再也不是惟一的選擇,愈來愈多的程序語言選擇了操做系統和機器指令集無關的、平臺中立的格式做爲程序編譯後的存儲格式。html
Class文件是一組以8位字節爲基礎單位的二進制流,各項數據嚴格的按照順序緊湊的地排列在Class文件中,中間沒有添加任何分隔符,這使得整個Class文件中存儲的內容幾乎所有是程序運行的必要數據,沒有空隙存在。當遇到須要佔用8個字節以上空間的數據項時,則會按照高位在前的方式分割成若干個8位字節進行存儲。java
根據Java虛擬機規範的規定,Class文件格式採用一種相似於C語言結構體的僞結構來存儲數據,這種僞結構中只有兩種數據類型L:無符號和表,後面的解析都要以這兩種數據類型爲基礎,因此這裏先介紹這兩個概念;無符號屬於基本數據類型,以u1,u2,u4,u8來分別表明1個字節,2個字節,4個字節和8個字節的無符號數,無符號數能夠用來描述數字、索引引用、數量或者按照utf-8編碼構成字符串值。表是由多個無符號或者其餘表做爲數據項構成的複合數據類型,全部表都習慣性地以「_info」結尾。表用於描述有層次關係的複合結構的數據,整個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個方面:編程語言
常量池中每個常量都是一個表,在jdk1.7後提供了14種表結構,他們都有一個共同的特色,就是表開始第一個位置是一個u1類型的標誌位,表明當前的常量是屬於哪種類型的。以下表:post
常量池結束後就是訪問標誌(access_flag)了,用於標識一些類或接口的訪問信息,好比這個Class是類仍是接口,是public仍是private,是否爲abstract等,每種訪問信息都是由一個16進制的標誌值表示,若是同時表示多種訪問信息,則獲得的標誌值爲這幾種訪問信息的邏輯或,其標誌位和含義以下表:this
標誌名稱 | 標誌值 |
含義 |
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 | 標誌這是一個枚舉 |
類索引和父類索引都是一個u2的類型,而接口索引是一個u2類的數據集合,Class中由這三項數據來肯定類的繼承關係。類索引、父類索引和接口索引集合都是有序的排列在訪問標識以後,類索引和父類索引兩個u2類型的索引值表示,他們各自指向一個類型爲COMNSTANT_Class_info的類描述符常量,經過該常量的索引值找到定義在COMNSTANT_Utf8_info類型的常量中的全限定名字符串,而接口索引集合用來描述這個類實現了哪些接口,這些被實現的接口按implements語句後的接口順序從左往右排列在接口集合中。
字段表(field_info)用於描述類或者接口中聲明的變量。字段包括了類級別變量和實例變量,可是不包括聲明在方法中的變量。字段的名稱,類型和修飾符等都是沒法固定的,只能引用常量池中的常量來描述,能夠包括的信息有:
字段表結構以下:
類型 | 名稱 | 數量 |
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後是一個屬性表集合,用於存儲一些額外的信息。
放發表(method_info)的結構與屬性表的就夠相同,方法裏的Java代碼通過編譯器編譯後編程字節碼指令,而後存放在方法屬性表的一個名爲「Code」的屬性裏,關於屬性表的項目,一樣會在後面跟進行詳細的介紹。
與字段表集合相對應,若是父類方法在子類中沒有被覆蓋,方法表中就不會出現父類的方法的信息,但一樣,有可能會出現會出現由編譯器自動添加的方法,最典型的就是類構造器「<cinit>」方法和實例狗構造器"<init>"方法。
在Java語言中,要重載一個方法,除了要方法與原方法的簡單名稱同樣以外,還必需要求擁有一個與原方法不一樣的特徵簽名,特診簽名就是一個方法中各個參數在常量池中字段符號引用的集合,可是返回值不包含在特徵簽名中,所以Java語言中想要覆蓋一個方法的話,若是是返回值不一樣是沒法覆蓋的。
方法表的結構:
方法訪問標誌:
在Class文件,字段表和方法表中均可以攜帶本身的屬性表集合,用於描述某些場景下專有的信息。屬性表集合沒有那麼嚴格的限定,再也不要求各個屬性表具備嚴格的順序,而且只要不予已有的屬性表的名字重複,任何人實現的編譯器均可以想屬性表中寫入本身定義的屬性信息,但Java虛擬機在運行時會忽略掉它不認識的屬性。Java虛擬機規範中預約義了9中虛擬機應當被識別的屬性(jdk1.5後又增長了一些新的特性),以下表:
對於每一個屬性,它的名稱都須要從常量池中引用的一個CONSTANT_Utf8_info類型的常量來表示,每一個屬性值的結構徹底能夠自定義,只需說明屬性值所需暫用的位數長度便可,一個符合規範的屬性表至少應具有attribute_name_info」、「attribute_length」和至少一項信息屬性。
前面已經提到過,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語句調用以前調用。
這裏的Exception屬性的做用是列舉出方法中可能會出現的受檢查異常,也就是方法描述是throws關鍵字後面列舉的異常,它的結構很簡單,只有attribute_name_index、attribute_length、number_of_exceptions、exception_index_table四項。
它用於描述Java源碼行號與字節碼行號之間的對應關係。
它用於描述棧幀中局部變量表中的變量與Java源碼中定義的變量之間的對應關係。
它用於記錄生成這個Class文件的源碼文件名稱。
ConstantValue屬性的做用是通知虛擬機自動爲靜態變量賦值,只有被static修飾的變量纔可使用這項屬性,在Java中對非static屬性的賦值是在構造器中完成的,而對於類變量,則有兩種方法能夠選擇,在類構造器賦值,或者在ConstantValue屬性賦值。
該屬性用於記錄內部類與宿主類之間的關聯。若是一個類中定義了內部類,那麼編譯器將會爲它及它所包含的內部類生成InnerClasses屬性。
該屬性用於表示某個類、字段和方法,已經被程序做者定爲再也不推薦使用,它能夠經過在代碼中使用@Deprecated註釋進行設置。
該屬性表明此字段或方法並非Java源代碼直接生成的,而是由編譯器自行添加的,如this字段和實例構造器、類構造器等。
參考資料: 《深刻理解Java虛擬機-JVM高級特性與最佳實踐》 -周志明
喜歡我寫的博客的同窗能夠關注訂閱號【Java解憂雜貨鋪】,裏面不按期發佈一些技術幹活,也能夠免費獲取大量最新最流行的技術教學視頻