1. 程式人生 > >深入理解JVM(七)——Class檔案結構

深入理解JVM(七)——Class檔案結構

這裡寫圖片描述

什麼是JVM的“無關性”?

Java具有平臺無關性,也就是任何作業系統都能執行Java程式碼。之所以能實現這一點,是因為Java執行在虛擬機器之上,不同的作業系統都擁有各自的Java虛擬機器,因此Java能實現“一次編寫,處處執行”。

而JVM不僅具有平臺無關性,還具有語言無關性。
平臺無關性是指不同作業系統都有各自的JVM,而語言無關性是指Java虛擬機器能執行除Java以外的程式碼!

這聽起來非常驚人,但JVM對能執行的語言是有嚴格要求的。首先來了解下Java程式碼的執行過程。

Java原始碼首先需要使用Javac編譯器編譯成class檔案,然後啟動JVM執行class檔案,從而程式開始執行。
也就是JVM只認識class檔案,它並不管何種語言生成了class檔案,只要class檔案符合JVM的規範就能執行。
因此目前已經有Scala、JRuby、Jython等語言能夠在JVM上執行。它們有各自的語法規則,不過它們的編譯器都能將各自的原始碼編譯成符合JVM規範的class檔案,從而能夠藉助JVM執行它們。

縱觀Class檔案結構

class檔案是二進位制檔案,它的內容具有嚴格的規範,檔案中沒有任何空格,全是連續的0/1。class檔案中的所有內容被分為兩種型別:無符號數 和 表。
- 無符號數
它表示class檔案中的值,這些值沒有任何型別,但有不同的長度。根據這些值長度的不同分為:u1、u2、u4、u8,分別代表1位元組的無符號數、2位元組的無符號數、4位元組的無符號數、8位元組的無符號數。
- 表
class檔案中所有資料(即無符號數)要麼單獨存在,要麼由多個無符號陣列成二維表。即class檔案中的資料要麼是單個值,要麼是二維表。

class檔案的組織結構

  1. 魔數
  2. 本檔案的版本資訊
  3. 常量池
  4. 訪問標誌
  5. 類索引
  6. 父類索引
  7. 介面索引集合
  8. 欄位表集合
  9. 方法表集合

Class檔案的構成1:魔數

class檔案的頭4個位元組稱為魔數,用來表示這個class檔案的型別。

魔數的作用就相當於檔案字尾名,只不過字尾名容易被修改,不安全,因此在class檔案中標示檔案型別比較合適。

class檔案的魔數是用16進製表示的“CAFEBABE”,非常具有浪漫主義色彩,誰說程式設計師的情商都很低!

Class檔案的構成2:版本資訊

緊接著魔數的4個位元組是版本號。它表示本class中使用的是哪個版本的JDK。

在高版本的JVM上能夠執行低版本的class檔案,但在低版本的JVM上無法執行高版本的class檔案,即使該class檔案中沒有用到任何高版本JDK的特性也無法執行!

Class檔案的構成3:常量池

1. 什麼是常量池?

緊接著版本號之後的就是常量池。常量池中存放兩種型別的常量:

  • 字面值常量
    字面值常量即我們在程式中定義的字串、被final修飾的值。
  • 符號引用
    符號引用就是我們定義的各種名字:
    1. 類和介面的全限定名
    2. 欄位的名字 和 描述符
    3. 方法的名字 和 描述符

2. 常量池的特點

  • 常量池長度不固定
    常量池的大小是不固定的,因此常量池開頭放置一個u2型別的無符號數,用來儲存當前常量池的容量。JVM根據這個值就知道常量池的頭尾來。
    注:這個值是從1開始的,若為5表示池中有4個常量。
  • 常量池中的常量由而為表來表示
    常量池開頭有個常量池容量計數器,接下來就全是一個個常量了,只不過常量都是由一張張二維表構成,除了記錄常量的值以外,還記錄當前常量的相關資訊。
  • 常量池是class檔案的資源倉庫
  • 常量池是與本class中其它部分關聯最多的部分
  • 常量池是class檔案中空間佔用最大的部分之一

3. 常量池中常量的型別

剛才介紹了,常量池中的常量大體上分為:字面值常量 和 符號引用。在此基礎上,根據常量的資料型別不同,又可以被細分為14種常量型別。這14種常量型別都有各自的二維表示結構。每種常量型別的頭1個位元組都是tag,用於表示當前常量屬於14種類型中的哪一個。

以CONSTANT_Class_info常量為例,它的二維表示結構如下:
CONSTANT_Class_info表:

型別 名稱 數量
u1 tag 1
u2 name_index 1

tag表示當前常量的型別(當前常量為CONSTANT_Class_info,因此tag的值應為7,表示一個類或介面的全限定名);
name_index表示這個類或介面全限定名的位置。它的值表示指向常量池的第幾個常量。它會指向一個CONSTANT_Utf8_info型別的常量,它的二維表結構如下:
CONSTANT_Utf8_info表:

型別 名稱 數量
u1 tag 1
u2 length 1
u1 bytes length

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位元組表示。

Class檔案的構成4:訪問標誌

在常量池之後是2位元組的訪問標誌。訪問標誌是用來表示這個class檔案是類還是介面、是否被public修飾、是否被abstract修飾、是否被final修飾等。
由於這些標誌都由是/否表示,因此可以用0/1表示。
訪問標誌為2位元組,可以表示16位標誌,但JVM目前只定義了8種,未定義的直接寫0.

Class檔案的構成5:類索引、父類索引、介面索引集合

類索引、父類索引、介面索引集合是用來表示當前class檔案所表示類的名字、父類名字、介面們的名字。
它們按照順序依次排列,類索引和父類索引各自使用一個u2型別的無符號常量,這個常量指向CONSTANT_Class_info型別的常量,該常量的bytes欄位記錄了本類、父類的全限定名。
由於一個類的介面可能有好多個,因此需要用一個集合來表示介面索引,它在類索引和父類索引之後。這個集合頭兩個位元組表示介面索引集合的長度,接下來就是介面的名字索引。

Class檔案的構成6:欄位表的集合

1. 什麼是欄位表集合?

接下來是欄位表的集合。欄位表集合用於儲存本類所涉及到的成員變數,包括例項變數和類變數,但不包括方法中的區域性變數。
每一個欄位表只表示一個成員變數,本類中所有的成員變數構成了欄位表集合。

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為止是欄位表的固定資訊,光有上述資訊可能無法完整地描述一個欄位,因此用屬性表集合來存放額外的資訊,比如一個欄位的值。(下面會詳細介紹)

3. 什麼是描述符?

成員變數(包括靜態成員變數和例項變數) 和 方法都有各自的描述符。
對於欄位而言,描述符用於描述欄位的資料型別;
對於方法而言,描述符用於描述欄位的資料型別、引數列表、返回值。

在描述符中,基本資料型別用大寫字母表示,物件型別用“L物件型別的全限定名”表示,陣列用“[陣列型別的全限定名”表示。
描述方法時,將引數根據上述規則放在()中,()右側按照上述方法放置返回值。而且,引數之間無需任何符號。

4. 欄位表集合的注意點

  1. 一個class檔案的欄位表集合中不能出現從父類/介面繼承而來欄位;
  2. 一個class檔案的欄位表集合中可能會出現程式猿沒有定義的欄位
    如編譯器會自動地在內部類的class檔案的欄位表集合中新增外部類物件的成員變數,供內部類訪問外部類。
  3. Java中只要兩個欄位名字相同就無法通過編譯。但在JVM規範中,允許兩個欄位的名字相同但描述符不同的情況,並且認為它們是兩個不同的欄位。

Class檔案的構成7:方法表的集合

在class檔案中,所有的方法以二維表的形式儲存,每張表來表示一個函式,一個類中的所有方法構成方法表的集合。
方法表的結構和欄位表的結構一致,只不過訪問標誌和屬性表集合的可選項有所不同。

型別 名稱 數量
u2 access_flags 1
u2 name_index 1
u2 descriptor_index 1
u2 attributes_count 1
attribute_info attributes attributes_count

方法表的屬性表集合中有一張Code屬性表,用於儲存當前方法經編譯器編譯過後的位元組碼指令。

方法表集合的注意點

  1. 如果本class沒有重寫父類的方法,那麼本class檔案的方法表集合中是不會出現父類/父介面的方法表;
  2. 本class的方法表集合可能出現程式猿沒有定義的方法
    編譯器在編譯時會在class檔案的方法表集合中加入類構造器和例項構造器。
  3. 過載一個方法需要有相同的簡單名稱和不同的特徵簽名。JVM的特徵簽名和Java的特徵簽名有所不同:
    • Java特徵簽名:方法引數在常量池中的欄位符號引用的集合
    • JVM特徵簽名:方法引數+返回值

Class檔案的構成8:屬性表的集合

這裡寫圖片描述