1.Class文件的定義
Class文件時一組以8位字節為基礎單位的二進制流,當遇到需要占用8位字節以上空間的數據項時,則會按照高位在前的方式分割成若干個*位字節進行存儲。Class文件格式采用一種類似C語言結構體的偽結構來存儲數據,這種偽結構中只有兩種數據類型:無符號數和表。
1.1.無符號數
無符號數屬于基本的數據類型,根據這些值長度的不同分為:u1、u2、u4、u8,分別代表1字節的無符號數、2字節的無符號數、4字節的無符號數、8字節的無符號數。無符號數可以用來描述數字、索引引用、數量值或者按照UTF-8編碼構成字符串值。
1.2.表
表示由多個無符號數或者其他表作為數據項構成的復合數據類型,所有表都習慣地以"_info結尾"。表用于描述有層次關系的復合結構的數據,整個Class文件本質上就是一張表,它由表6-1所示的數據項構成。
2.Class文件的構成
2.1.魔數
class文件的頭4個字節稱為魔數,它的唯一作用是確定這個文件能否為一個能被虛擬機接受的Class文件。
魔數的作用就相當于文件后綴名,只不過后綴名容易被修改,不安全,因此在class文件中標示文件類型比較合適。
class文件的魔數是用16進制表示的“CAFEBABE”,非常具有浪漫主義色彩,誰說程序員的情商都很低!
2.2.版本號
緊接著魔數的四個字節是class文件的此版本號和主版本號。 隨著Java的發展, class文件的格式也會做相應的變動。 版本號標志著class文件在什么時候, 加入或改變了哪些特性。 舉例來說, 不同版本的javac編譯器編譯的class文件, 版本號可能不同, 而不同版本的JVM能識別的class文件的版本號也可能不同, 一般情況下, 高版本的JVM能識別低版本的javac編譯器編譯的class文件, 而低版本的JVM不能識別高版本的javac編譯器編譯的class文件。 如果使用低版本的JVM執行高版本的class文件,JVM會拋出java.lang.UnsupportedClassVersionError 。
2.3.常量池
2.3.1. 什么是常量池?
緊接著版本號之后的就是常量池。常量池中存放兩種類型的常量:字面值常量和符號引用。
字面值常量即我們在程序中定義的字符串、被final修飾的值。
符號引用就是我們定義的各種名字:類和接口的全限定名、字段的名字 和 描述符、方法的名字 和 描述符。
2.3.2 常量池的特點
常量池長度不固定
常量池的大小是不固定的,因此常量池開頭放置一個u2類型的無符號數,用來存儲當前常量池的容量。JVM根據這個值就知道常量池的頭尾來。
注:這個值是從1開始的,若為5表示池中有4個常量。
常量池中的常量由而為表來表示
常量池開頭有個常量池容量計數器,接下來就全是一個個常量了,只不過常量都是由一張張二維表構成,除了記錄常量的值以外,還記錄當前常量的相關信息。
常量池是class文件的資源倉庫
常量池是與本class中其它部分關聯最多的部分
常量池是class文件中空間占用最大的部分之一
2.3.3常量池中常量的特點
剛才介紹了,常量池中的常量大體上分為:字面值常量 和 符號引用。在此基礎上,根據常量的數據類型不同,又可以被細分為14種常量類型。這14種常量類型都有各自的二維表示結構。每種常量類型的頭1個字節都是tag,用于表示當前常量屬于14種類型中的哪一個。
以CONSTANT_Class_info常量為例,它的二維表示結構如下:
CONSTANT_Class_info表:
類型名稱數量
u1tag1
u2name_index1
tag表示當前常量的類型(當前常量為CONSTANT_Class_info,因此tag的值應為7,表示一個類或接口的全限定名);
name_index表示這個類或接口全限定名的位置。它的值表示指向常量池的第幾個常量。它會指向一個CONSTANT_Utf8_info類型的常量,它的二維表結構如下:
CONSTANT_Utf8_info表:
類型名稱數量
u1tag1
u2length1
u1byteslength
CONSTANT_Utf8_info表示字符串常量;
tag表示當前常量的類型,這里應該是1;
length表示這個字符串的長度;
bytes為這個字符串的內容(采用縮略的UTF8編碼)
問:為什么Java中定義的類、變量名字必須小于64K?
類、接口、變量等名字都屬于符號引用,它們都存儲在常量池中。而不管哪種符號引用,它們的名字都由CONSTANT_Utf8_info類型的常量表示,這種類型的常量使用u2存儲字符串的長度。由于2字節最多能表示65535個數,因此這些名字的最大長度最多只能是64K。
問:什么是UTF-8編碼?什么是縮略UTF-8編碼?
前者每個字符使用3個字節表示,而后者把128個ASKII碼用1字節表示,某些字符用2字節表示,某些字符用3字節表示。
2.4.訪問標志
在常量池之后是2字節的訪問標志。訪問標志是用來表示這個class文件是類還是接口、是否被public修飾、是否被abstract修飾、是否被final修飾等。
由于這些標志都由是/否表示,因此可以用0/1表示。
訪問標志為2字節,可以表示16位標志,但JVM目前只定義了8種,未定義的標志位一律為0.
2.5.類索引、父類索引與接口索引集合
類索引(this_class) 和父類索引(super class) 都是一個u2類型的數據,而接口索引(interfaces) 是一組u2類型的數據的集合,Class 文件中由這三項數據來確定這個類的承關系。類索引用于確定這個類的全限定名,父類索引用于確定這個類的父類的全限定名,由于Java 語言不允許多重繼承,所以父類索引只有一個,除了java.lang.Object 之外,所的Java 類都有父類,因此除了java lang.Object 外,所有Java 類的父類索引都不為0.接口索引集合就用來描述這個類實現了哪些接口,這些被實現的接口將按implements語句(如果這個類本身是一個接口,則應當是extends語句) 后的接口順序從左到右排列在接口索集合中。
它們按照順序依次排列,類索引和父類索引各自使用一個u2類型的無符號常量,這個常量指向CONSTANT_Class_info類型的常量,該常量的bytes字段記錄了本類、父類的全限定名。
接口索引集合,入口的第一項-u2類型的數據為接口計數器,表示索引表的容量。如果該類沒有實現任何接口,則該計數器值為0,后面接口的索引表不再占用任何字節。
2.6.字段表集合
字段表用于描述接口或者類中聲明的變量。字段包括類及變量以及實例及變量,但不包括在方法內部聲明的局部變量。
每一個字段表只表示一個成員變量,本類中所有的成員變量構成了字段表集合。
2.6.2 字段表結構的定義
類型? ?????????????????????????? 名稱??????????????????????? 數量
u2 ? ? ? ? ? ? ? ? ? ? ? ? access_flags??????? ? ? ? ? ? ? ? 1
u2? ? ? ?????????????????? name_index???????? ? ? ? ???????? 1
u2? ? ? ?????????????????? descriptor_index?? ? ? ? ? ? ? ?? 1
u2???????????????????????? attributes_count?????????????????? 1
attribute_info???????? attributes????????????? attributes_count
access_flags
字段的訪問標志。在Java中,每個成員變量都有一系列的修飾符,和上述class文件的訪問標志的作用一樣,只不過成員變量的訪問標志與類的訪問標志稍有區別。
name_index
本字段名字的索引。指向一個CONSTANT_Class_info類型的常量,這里面存儲了本字段的名字等信息。
descriptor_index
描述符。用于描述本字段在Java中的數據類型等信息(下面詳細介紹)
attributes_count
屬性表集合的長度。
attributes
屬性表集合。到descriptor_index為止是字段表的固定信息,光有上述信息可能無法完整地描述一個字段,因此用屬性表集合來存放額外的信息,比如一個字段的值。(下面會詳細介紹)
2.6.3 什么是描述符
成員變量(包括靜態成員變量和實例變量) 和 方法都有各自的描述符。
對于字段而言,描述符用于描述字段的數據類型;
對于方法而言,描述符用于描述字段的數據類型、參數列表、返回值。
在描述符中,基本數據類型用大寫字母表示,對象類型用“L對象類型的全限定名”表示,數組用“[數組類型的全限定名”表示。
描述方法時,將參數根據上述規則放在()中,()右側按照上述方法放置返回值。而且,參數之間無需任何符號。
2.6.4 字段表需要注意的點
一個class文件的字段表集合中不能出現從父類/接口繼承而來字段;
一個class文件的字段表集合中可能會出現程序猿沒有定義的字段
如編譯器會自動地在內部類的class文件的字段表集合中添加外部類對象的成員變量,供內部類訪問外部類。
Java中只要兩個字段名字相同就無法通過編譯。但在JVM規范中,允許兩個字段的名字相同但描述符不同的情況,并且認為它們是兩個不同的字段。
Class文件的構成
2.7:方法表的集合
在class文件中,所有的方法以二維表的形式存儲,每張表來表示一個函數,一個類中的所有方法構成方法表的集合。
方法表的結構和字段表的結構一致,只不過訪問標志和屬性表集合的可選項有所不同。
類型名稱數量
u2access_flags1
u2name_index1
u2descriptor_index1
u2attributes_count1
attribute_infoattributesattributes_count
方法表的屬性表集合中有一張Code屬性表,用于存儲當前方法經編譯器編譯過后的字節碼指令。
方法表集合的注意點
如果本class沒有重寫父類的方法,那么本class文件的方法表集合中是不會出現父類/父接口的方法表;
本class的方法表集合可能出現程序猿沒有定義的方法
編譯器在編譯時會在class文件的方法表集合中加入類構造器和實例構造器。
重載一個方法需要有相同的簡單名稱和不同的特征簽名。JVM的特征簽名和Java的特征簽名有所不同:
Java特征簽名:方法參數在常量池中的字段符號引用的集合
JVM特征簽名:方法參數+返回值
2.8:屬性表的集合
在Class文件,字段表,方法表中都可以攜帶自己的屬性表集合,以用于描述某些場景專有的信息。與Class文件中其它的數據項目要求的順序、長 度和內容不同,屬性表集合的限制稍微寬松一些,不再要求各個屬性表具有嚴格的順序,并且只要不與已有的屬性名重復,任何人實現的編譯器都可以向屬性表中寫入自己定義的屬性信息,Java虛擬機運行時會忽略掉它不認識的屬性。