深入理解JVM(五)Class類的檔案結構

_雲起_發表於2018-06-18

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所示的資料項構成。

深入理解JVM(五)Class類的檔案結構

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虛擬機器執行時會忽略掉它不認識的屬性。


相關文章