天天看點

《深入了解 Java 虛拟機》讀書筆記:類檔案結構

正文

一、無關性的基石

1、兩種無關性

  • 平台無關性: Java 程式的運作不受計算機平台的限制,“一次編寫,到處運作”。
  • 語言無關性: Java 虛拟機隻與 Class 檔案關聯,并不關心 Class 檔案的來源是何種語言。

2、無關性的實作基礎

  • 各種不同平台的虛拟機
  • 所有平台都統一使用的位元組碼存儲格式

二、Class 類檔案的結構

Class 類檔案是一組以 8 位元組為基礎機關的二進制流,各個資料項目嚴格按照順序緊湊地排列在 Class 檔案中,中間沒有添加任何分隔符。當遇到需要占用 8 位元組以上空間的資料項目時,則按照高位在前(最高位位元組在位址最低位)的方式分割成若幹個 8 位位元組進行存儲。

Class 檔案格式采用一種類似 C 語言結構體的僞結構來存儲資料,這種僞結構中隻有兩種資料類型:無符号數和表。

  • 無符号數: 基本資料類型,以 u1、u2、u4、u8 來分别代表 1 個位元組、2 個位元組、4 個位元組和 8 個位元組的無符号數。可用來描述數字、索引引用、數量值或按照 UTF-8 編碼構成字元串值。
  • 表: 由多個無符号數或其他表作為資料項構成的複合資料類型,所有表都習慣性地以“_info”結尾。表用于描述有層次關系的複合結構資料,整個 Class 檔案本質上就是一張表。

無論是無符号數還是表,當需要描述同一類型但數量不定的多個資料時,會使用一個前置的容量計數器加若幹個連續資料項的形式,這若幹個連續資料項稱為集合。

Class 檔案格式:

類型 名稱 數量
u4 magic(魔數) 1
u2 minor_version(次版本号) 1
u2 major_version(主版本号) 1
u2 constant_pool_count(常量池容量計數器) 1
cp_info constant_pool(常量池) constant_pool_count - 1
u2 access_flags(通路标志) 1
u2 this_class(類索引) 1
u2 super_class(父類索引) 1
u2 interfaces_count(接口計數器) 1
u2 interfaces(接口索引集合) interfaces_count
u2 fields_count(字段表計數器) 1
field_info fields(字段表集合) fields_count
u2 methods_count(方法表計數器) 1
method_info methods(方法表集合) methods_count
u2 attributes_count(屬性表計數器) 1
attribute_info attributes(屬性表集合) attributes_count

1、魔數

每個 Class 檔案的頭 4 個位元組稱為魔數,用于确定該檔案是否為一個能被虛拟機接受的 Class 檔案。其值為:0xCAFEBABE(咖啡寶貝?)。

2、Class 檔案的版本

緊接着魔數的 4 個位元組存儲的是 Class 檔案的版本号:第 5、6 個位元組是次版本号,第 7、8 個位元組是主版本号。

3、常量池

緊接着主次版本号之後的是常量池入口,常量池可以了解為 Class 檔案中的資源倉庫。

由于常量池中常量的數量是不固定的,是以在常量池入口放置了一個 u2 類型的常量池容量計數器。該計數器的索引值是從 1 而不是從 0 開始,當表示“不引用任何一個常量池項目”時,則可将計數器置為 0。

常量池主要存放兩大類常量:字面量和符号引用。每一項常量都是一個表,這些表開始的第一位是一個 u1 類型的标志位,代表目前常量所屬的常量類型。常量池目前有 14 種常量類型,它們各自均有自己的結構。

常量池的項目類型:

類型 标志 描述
CONSTANCT_Utf8_info 1 UTF-8 編碼的字元串
CONSTANCT_Integer_info 3 整型字面量
CONSTANCT_Float_info 4 浮點型字面量
CONSTANCT_Long_info 5 長整型字面量
CONSTANCT_Double_info 6 雙精度浮點型字面量
CONSTANCT_Class_info 7 類或接口的符号引用
CONSTANCT_String_info 8 字元串類型字面量
CONSTANCT_Fieldref_info 9 字段的符号引用
CONSTANCT_Methodref_info 10 類中方法的符号引用
CONSTANCT_InterfaceMethodref_info 11 接口中方法的符号引用
CONSTANCT_NameAndType_info 12 字段或方法的部分符号引用
CONSTANCT_MethodHandle_info 15 表示方法句柄
CONSTANCT_MethodType_info 16 辨別方法類型
CONSTANCT_InvokeDynamic_info 18 表示一個動态方法調用點

常量類型結構:

(1)CONSTANT_Class_info 類型常量

類型 名稱 數量 描述
u1 tag 1 标志位,值為 0x07
u2 name_index 1 索引值,指向常量池中一個 CONSTANT_Utf8_info 類型常量,表示這個類(或接口)的全限定名

(2)CONSTANT_Utf8_info 類型常量

類型 名稱 數量 描述
u1 tag 1 标志位,值為 0x01
u2 length 1 UTF-8 編碼的字元串占用的位元組數
u1 bytes length 長度為 length 的 UTF-8 編碼的字元串

(3)...

4、通路标志

常量池之後,緊接着的兩個位元組代表通路标志,用于識别一些類或接口層次的通路資訊,包括:這個 Class 是類還是接口、是否定義為 public 類型、是否定義為 abstract 類型、是否被聲明為 final(隻有類可設定)等。

通路标志:

标志名稱 标志值 含義
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 辨別這是一個枚舉

5、類索引、父類索引與接口索引集合

類索引和父類索引都是 u2 類型的資料,而接口索引集合是一組 u2 類型的資料的集合,Class 檔案由這三項資料确定這個類的繼承關系。

  • 類索引:指向一個類型為 CONSTANT_Class_info 的類描述符常量,表示該類的全限定名。
  • 父類索引:指向一個類型為 CONSTANT_Class_info 的類描述符常量,表示父類的全限定名。
  • 接口索引集合:用于描述該類實作了哪些接口,接口索引集合的入口放置了一個 u2 類型的接口計數器,表示索引表的容量。

6、字段表集合

字段表用于描述接口或類中聲明的變量。字段包括類級變量以及執行個體變量,但不包括在方法内部聲明的局部變量。

字段表結構:

類型 名稱 數量
u2 access_flags(字段通路标志) 1
u2 name_index(簡單名稱索引) 1
u2 descriptor_index(描述符索引) 1
u2 attributes_count(屬性表計數器) 1
attribute_info attributes(屬性表集合) attributes_count
  • 字段通路标志(access_flags):
标志名稱 标志值 含義
ACC_PUBLIC 0x0001 字段是否 public
ACC_PRIVATE 0x0002 字段是否 private
ACC_PROTECTED 0x0004 字段是否 protected
ACC_STATIC 0x0008 字段是否 static
ACC_FINAL 0x0010 字段是否 final
ACC_VOLATILE 0x0040 字段是否 volatile
ACC_TRANSIENT 0x0080 字段是否 transient
ACC_SYNTHETIC 0x1000 字段是否由編譯器自動産生的
ACC_ENUM 0x4000 字段是否 enum
  • 簡單名稱索引(name_index):指向常量池中一個 CONSTANT_Utf8_info 類型常量,代表字段的簡單名稱。
  • 描述符索引(descriptor_index):指向常量池中一個 CONSTANT_Utf8_info 類型常量,代表字段和方法的描述符。描述符的作用是用來描述字段的資料類型、方法的參數清單(包括數量、類型以及順序)和傳回值。
  • 屬性表集合(attributes):用于存儲一些額外的資訊。

7、方法表集合

方法表的結構與字段表一樣,依次包括了通路标志(access_flags)、名稱索引(name_index)、描述符索引(descriptor_index)、屬性表集合(attributes)幾項,這些資料項目的含義也非常類似,僅在通路标志和屬性表集合的可選項中有所差別。

方法表結構:

類型 名稱 數量
u2 access_flags(字段通路标志) 1
u2 name_index(簡單名稱索引) 1
u2 descriptor_index(描述符索引) 1
u2 attributes_count(屬性表計數器) 1
attribute_info attributes(屬性表集合) attributes_count
  • 方法通路标志(access_flags):
标志名稱 标志值 含義
ACC_PUBLIC 0x0001 方法是否 public
ACC_PRIVATE 0x0002 方法是否 private
ACC_PROTECTED 0x0004 方法是否 protected
ACC_STATIC 0x0008 方法是否 static
ACC_FINAL 0x0010 方法是否 final
ACC_SYNCHRONIZED 0x0020 方法是否 synchronized
ACC_BRIDGE 0x0040 方法是否是由編譯器産生的橋接方法
ACC_VARARGS 0x0080 方法是否接受不定參數
ACC_NATIVE 0x0100 方法是否 native
ACC_ABSTRACT 0x0400 方法是否 abstract
ACC_STRICTFP 0x0800 方法是否 stricftp
ACC_SYNTHETIC 0x1000 方法是否由編譯器自動産生的

方法裡的 Java 代碼,經過編譯器編譯成位元組碼指令後,存放在方法屬性表集合中一個名為“Code”的屬性裡面。

8、屬性表集合

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

屬性表不要求各個屬性表具有嚴格的順序,并且隻要不與已有屬性名重複,任何人實作的編譯器都可以向屬性表中寫入自己定義的屬性資訊,Java 虛拟機運作時會忽略掉它不認識的屬性。

屬性表結構:

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

(1)Code 屬性

Java 程式方法體中的代碼經過 Javac 編譯器處理後,最終變成位元組碼指令存儲在 Code 屬性内。

Code 屬性表的結構:

類型 名稱 數量
u2 attribute_name_index 1
u4 attribute_length 1
u2 max_stack 1
u2 max_locals 1
u4 code_length 1
u1 code code_length
u2 exception_table_length 1
exception_info exception_table exception_table_length
u2 attritutes_count 1
attribute_info attritutes attritutes_count
  • attribute_name_index:代表該屬性的屬性名稱,是一項指向 CONSTANT_Uft8_info 型常量的索引,常量值固定為“Code”。
  • attribute_length:代表屬性值的長度。
  • max_stack:代表操作數棧深度的最大值。
  • max_locals:代表局部變量表所需的存儲空間。
  • code_length:代表位元組碼長度。
  • code:用于存儲位元組碼指令的一系列位元組流。

(2)Exceptions 屬性

用于列舉出方法中可能抛出的受查異常,也就是方法描述時在 throws 關鍵字後列舉的異常。

Exceptions 屬性表的結構:

類型 名稱 數量
u2 attribute_name_index 1
u4 attribute_length 1
u2 number_of_exceptions 1
u2 exception_index_table number_of_exceptions

(3)...

三、位元組碼指令簡介

Java 虛拟機的指令由一個操作碼和零至多個操作數構成。由于 Java 虛拟機采用面向操作數棧而不是寄存器的架構,所有大多數指令都不包括操作數,隻有一個操作碼。但是大多數指令都包含了其操作所對應的資料類型資訊。

如果不考慮異常處理,Java 虛拟機的解釋器可以使用下面的僞代碼當作最基本的執行模型來了解:

do {
    自動計算 PC 寄存器的值加 1;
    根據 PC 寄存器的訓示位置,從位元組碼流中取出操作碼;
    if ( 位元組碼存在操作數 ) 從位元組碼流中取出操作數;
    執行操作碼所定義的操作;
}           

對于大多數與資料類型相關的位元組碼指令,它們的操作碼助記符中都有特殊的字元來表示專門為哪種資料類型服務:i 代表對 int 類型的資料操作,l 代表 long,s 代表 short,b 代表 byte,c 代表 char,f 代表 float,d 代表 double,a 代表 reference。

1、加載和存儲指令

加載和存儲指令用于将資料在棧幀中的局部變量表和操作數棧之間來回傳輸,這類指令包括:

  • 将一個局部變量加載到操作棧:iload、iload_<n>、lload、lload_<n>、fload、fload_<n>、dload、dload_<n>、aload、aload_<n>。
  • 将一個數值從操作數棧存儲到局部變量表:istore、istore_<n>、lstore、lstore_<n>、fstore、fstore_<n>、dstore、dstore_<n>、astore、astore_<n>。
  • 将一個常量加載到操作數棧:bipush、sipush、ldc、ldc_w、ldc2_w、aconst_null、iconst_ml、iconst_<i>、lconst_<l>、fconst_<f>、dconst_<d>。
  • 擴充局部變量表的通路索引的指令:wide。

以上列舉的指令助記符中,有一部分是以尖括号結尾的指令。這幾組指令是帶有一個操作數的通用指令(如 iload)的特殊形式,它們省略了顯式的操作數,而是将操作數隐含在指令中。例如:iload_0 代表操作數為 0 的 iload 指令。

2、運算指令

運算或算術指令用于對兩個操作數以上的值進行某種特定運算,并把結果重新存入到操作數棧頂。大體上算術指令可分為兩種:對整型資料進行運算的指令和對浮點型資料進行運算的指令。所有的算術指令如下:

  • 加法指令:iadd、ladd、fadd、dadd。
  • 減法指令:isub、lsub、fsub、dsub。
  • 乘法指令:imul、lmul、fmul、dmul。
  • 除法指令:idiv、ldiv、fdiv、ddiv。
  • 求餘指令:irem、lrem、frem、drem。
  • 取反指令:ineg、lneg、fneg、dneg。
  • 位移指令:ishl、ishr、iushr、lshl、lshr、lushr。
  • 按位或指令:ior、lor。
  • 按位與指令:iand、land。
  • 按位異或指令:ixor、lxor。
  • 局部變量自增指令:iinc。
  • 比較指令:dcmpg、dcmpl、fcmpg、fcmpl、lcmp。

3、類型轉換指令

類型轉換指令可以将兩種不同的數值類型進行互相轉換,這些轉換操作一般用于實作使用者代碼中的顯式類型轉換操作,或者用來處理位元組碼指令集中資料類型相關指令無法與資料類型一一對應的問題。

Java 虛拟機直接支援(即轉換時無需顯示的轉換指令)以下數值類型的寬化類型轉換(小範圍類型向大範圍類型的安全轉換):

  • int 到 long、float、double。
  • long 到 float、double。
  • float 到 double。

相對的,處理窄化類型轉換時,必須顯示地使用轉換指令來完成,這些指令包括:i2b、i2c、i2s、l2i、f2i、f2l、d2i、d2l、d2f。窄化類型轉換可能會導緻轉換結果産生不同的正負号、不同的數量級的情況,轉換過程很可能會導緻數值的精度丢失。

4、對象建立與通路指令

雖然類執行個體和數組都是對象,但 Java 虛拟機對類執行個體和數組的建立與操作使用了不同的位元組碼指令。相關指令如下:

  • 建立類執行個體的指令:new。
  • 建立數組的指令:newarray、anewarray、multianewarray。
  • 通路類字段和執行個體字段的指令:getstatic、putstatic、getfield、putfield。
  • 把一個數組元素加載到操作數棧的指令:baload、caload、saload、iaload、laload、faload、daload、aaload。
  • 将一個操作數棧的值存儲到數組元素中的指令:bastore、castore、sastore、iastore、fastore、dastore、aastore。
  • 取數組長度的指令:arraylength。
  • 檢查類執行個體類型的指令:instanceof、checkcast。

5、操作數棧管理指令

Java 虛拟機提供了一些用于直接操作操作數棧的指令,包括:

  • 将操作數棧的棧頂一個或兩個元素出棧:pop、pop2。
  • 複制棧頂一個或兩個數值并将複制值或雙份的複制值重新壓入棧頂:dup、dup2、dup_x1、dup2_x1、dup_x2、dup2_x2。
  • 将棧最頂端的兩個數值互換:swap。

6、控制轉移指令

控制轉移指令可以讓 Java 虛拟機有條件或無條件地從指定位置的指令繼續執行程式,而不是從控制轉移指令的下一條指令繼續執行程式。從概念模型上了解,可認為控制轉移指令就是在有條件或無條件地修改 PC 寄存器的值。控制轉移指令如下:

  • 條件分支:ifeq、iflt、ifle、ifne、ifgt、ifge、ifnull、ifnonnull、if_icmpeq、if_icmpne、if_icmplt、if_icmpgt、if_icmple、if_icmpge、if_acmpeq、if_acmpne。
  • 複合條件分支:tableswitch、lookupswitch。
  • 無條件分支:goto、goto_w、jsr、jsr_w、ret。

7、方法調用和傳回指令

方法調用指令與資料類型無關,包括:

  • invokevirtual 指令:用于調用對象的執行個體方法,根據對象的實際類型進行分派(虛方法分派),這是 Java 中最常見的方法分派方式。
  • invokeinterface 指令:用于調用接口方法,它會在運作時搜尋一個實作了這個接口方法的對象,找出合适的方法進行調用。
  • invokespecial 指令:用于調用一些需要特殊處理的執行個體方法,包括執行個體初始化方法、私有方法和父類方法。
  • invokestatic 指令:用于調用類方法。
  • invokedynamic 指令:用于在運作時動态解析出調用點限定符所引用的方法,并執行該方法。前 4 條調用指令的分派邏輯固化在 Java 虛拟機内部,而 invokedynamic 指令的分派邏輯是由使用者所設定的引導方法決定的。

方法傳回指令是根據傳回值的類型區分的,包括:ireturn(用于傳回值是 boolean、byte、char、short、int 的方法)、lreturn、freturn、dreturn、areturn、return(用于 void 方法、執行個體初始化方法、類和接口的類初始化方法)。

8、異常處理指令

Java 虛拟機中顯式抛出異常的操作(throw 語句)都由 athrow 指令實作。而處理異常(catch 語句)則不是由位元組碼指令來實作的(很久之前曾經使用 jsr 和 ret 指令實作),而是采用異常表來完成。

9、同步指令

Java 虛拟機可以支援方法級的同步和方法内部一段指令序列的同步,這兩種同步結構都是使用管程(Monitor)來支援的。

方法級的同步是隐式的,即無須通過位元組碼指令來控制。虛拟機可以從方法通路标志 ACC_SYNCHRONIZED 得知一個方法是否聲明為同步方法。如果方法通路标志 ACC_SYNCHRONIZED 被設定為 true,執行線程就要求先成功持有管程,然後才能執行方法,最後當方法完成時釋放管程。

同步一段指令集序列通常是由 synchronized 語句來表示的,Java 虛拟機的指令集中由 monitorenter 和 monitorexit 兩條指令來支援 synchronized 關鍵字語義。