1. 程式人生 > >JVM虛擬機器 - Class類檔案結構

JVM虛擬機器 - Class類檔案結構

JVM虛擬機器 - Class類檔案結構

概述

Class檔案是一組以8位位元組為基礎單位的二進位制流,各個資料專案嚴格按照順序緊湊地排列在Class檔案之中,中間沒有新增任何分隔符,這使得整個Class檔案中儲存的內容幾乎都是程式執行的必要資料。當遇到需要佔用8位位元組以上空間的資料項時,會按照高位在前的方式分割成若干個8位位元組進行儲存。

Class檔案格式中只有兩種資料型別:無符號數

  • 無符號數屬於最基本的資料型別,以u1、u2、u4、u8來分別代表1個位元組、2個位元組、4個位元組和8個位元組的無符號數,無符號數可以用來描述數字、索引引用、數量值或者按照UTF-8編碼構成字串值。
  • 表是由多個無符號數或者其他表作為資料項構成的複合資料型別,以“_info”結尾。表用來描述有層次關係的符合結構的資料。

整個Class檔案本質上就是一張表。

無論是無符號數還是表,當需要描述同一型別但數量不定的多個數據時,經常會使用一個前置的容量計數器加若干個連續的資料項的形式,稱這一系列連續的某一型別的資料位某一型別的集合。

class檔案的組織結構

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

1. 魔數

每個Class檔案的頭4個位元組稱為魔數(Magic Number),唯一作用就是確定這個檔案是否為一個能被虛擬機器接受的Class檔案。

魔數的表示是用16進位制的數:0xCAFEBABE 來表示。

2. 本檔案的版本資訊

在魔數後面的4個位元組儲存的就是Class檔案的版本號:

  • 第5、第6個位元組是次版本號(Minor Version)
  • 第7、第8個位元組是主版本號(Major Version)

Java版本號的計算

Java的版本號是從45開始的,JDK1.1之後的每個JDK大版本釋出主版本號向上加1。

高版本的JDK能向下相容以前版本的Class檔案,但不能相容以後版本的Class檔案,即使檔案格式並未發生變化,虛擬機器也拒絕執行超過其版本號的Class檔案。

3. 常量池

3.1. 常量池的概念

常量池可以理解為Class檔案之中的資源倉庫,他是Class檔案結構中與其他專案關聯最多的資料型別,也是佔用Class檔案空間最大的資料專案之一,同時它還是在Class檔案中第一個出現的表型別資料專案。

常量池中主要存放兩大類常量:

  • 字面量:接近於Java語言層面的常量概念,如文字字串、宣告為final的常量
  • 符號引用:就是我們定義的各種名字,包括下面三類:
    • 類和介面的全限定名
    • 欄位的名稱和描述符
    • 方法的名稱和描述符

3.2. 常量池的特點

  1. 常量池長度不固定

    常量池的大小是不固定的,因此常量池開頭放置一個u2型別的無符號數,用來儲存當前常量池的容量。JVM根據這個值就知道常量池的頭尾來。

    這個無符號數從1開始,不是通常的從0開始

  2. 常量池中的常量由二維表來表示

    常量池開頭有個常量池容器計數器,接下來就全是一個個常量了,只不過常量都是由一張張二維表構成,除了記錄常量的值以外,還記錄當前常量的相關資訊。

  3. Class檔案之中的資源倉庫

  4. Class檔案結構中與其他專案關聯最多的資料型別

  5. 佔用Class檔案空間最大的資料專案之一

3.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編碼)

4. 訪問標誌

訪問標誌(access_flags)佔用兩個位元組,這個標誌用於識別一些類或者介面層次的訪問資訊:

  • 這個Class是類還是介面
  • 是否定位為public型別
  • 是否定義為abstract型別
  • 是否被聲名為final

訪問標誌中一共有16個標誌位可用,當前只用了8個,其他要求一律為0。

5. 類索引、父類索引和介面索引集合

類索引(this_class)和父類索引(super_class)都是一個u2型別的資料,介面索引集合(interfaces)是一組u2型別的資料的集合。

Class檔案由這三項資料來確定這個類的繼承關係:

  • 類索引確定這個類的全限定名

  • 父類索引用於確定這個類的父類的全限定名

    java.lang.Object外,所有Java類的父類索引都不為0。

  • 介面索引集合描述這個類實現了哪些介面

    被實現的介面按介面順序從左到右排列在介面索引集合中。

類索引、父類索引和介面索引集合都按順序排列在訪問標誌之後,類索引和父類索引用兩個u2型別的所引致指向一個型別為CONSTANT_Class_info的類描述符常量,該常量的bytes欄位記錄了本類、父類的全限定名。

由於一個類的介面可能有好多個,因此需要用一個集合來表示介面索引,它在類索引和父類索引之後。這個集合頭兩個位元組表示介面索引集合的長度,接下來就是介面的名字索引。

6. 欄位表集合

欄位表用於描述介面或者類中生命的變數。欄位包括類級變數以及例項級變數,但不包括在方法內部宣告的區域性變數。

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

    欄位的訪問標誌。

  • name_index

    本欄位名稱的索引,指向一個CONSTANT_Class_info型別的常量,儲存了本欄位的名字等資訊。

  • descriptor_index

    欄位和方法的描述符,用於描述本欄位在Java中的資料型別等資訊。

  • attributes_count

    屬性表集合的長度。

  • attributes

    屬性表集合。

全限定名

全限定名就是把類全名中的.替換成了/而已,為了使連續的多個全限定名之間不產生混淆,在使用時最後一般會加入一個;來表示全限定名結束。

簡單名稱

簡單名稱就是指沒有型別和引數修飾的方法或者欄位名稱。

描述符

描述符的作用是用來描述欄位的資料型別、方法的引數列表(包括數量、型別以及順序)和返回值。

在描述符中:

  • 基本資料型別以及代表無返回值的void型別都用一個大寫字元來表示。
  • 物件型別則用字元L加物件的全限定名來表示。
標識字元 含義 標識字元 含義
B 基本型別byte J 基本型別long
C 基本型別char S 基本型別short
D 基本型別double Z 基本型別boolean
F 基本型別float V 特殊型別void
I 基本型別int L 物件型別,如Ljava/lang/Object
  1. 對於陣列型別,每一唯獨將使用一個前置的“[”字元來描述,如一個定義為java.lang.String[][]型別的二維陣列,將被記錄為:[[Ljava/lang/String]],一個整形陣列int[]將被記錄為:[I

  2. 對於描述方法,按照“先引數列表,後返回值的順序描述”,引數列表按照引數的嚴格順序放在一組小括號“()”內。例如:void inc()的描述符為:()V。方法java.lang.String.toString()的描述符為:()Ljava/lang/String

    方法int indexOf(char[] source, int sourceOffset, int sourceCount, char[] target, int targetOffset, int targetCount, int fromIndex)的描述符為:([CII[CIII)I

7. 方法表集合

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

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

方法中的Java程式碼,經過編譯器編譯成位元組碼指令後,存放在方法屬性表集合中一個名為“Code”的屬性裡面,屬性表作為Class檔案格式中最具拓展性的一種資料專案,在第8節中講。

8. 屬性表集合

在Class檔案、欄位表、方法表都可以攜帶自己的屬性表集合,以用於描述某些場景專有的資訊。

屬性表集合對於各個屬性表的順序不再做嚴格要求,只要不與已有屬性名重複即可。任何人實現的編譯器都可以向屬性表中寫入自己定義的屬性資訊,Java虛擬機器執行時會忽略掉它不認識的屬性。

為了正確解析Class檔案,虛擬機器與定義了一些虛擬機器實現應當能識別的屬性,對於這些屬性,它的名稱需要從常量池中引用一個CONSTANT_Utf9_info型別的常量來表示,而屬性值的結構則是完全自定義的。

一個符合規則的屬性表應該有如下結構:

型別 名稱 數量
u2 attribute_name_index 1
u4 attribute_length 1
u1 info attribute_length