《深刻理解 Java 虛擬機》讀書筆記:類文件結構

正文

1、無關性的基石

一、兩種無關性

  • 平臺無關性: Java 程序的運行不受計算機平臺的限制,「一次編寫,處處運行」。
  • 語言無關性: Java 虛擬機只與 Class 文件關聯,並不關心 Class 文件的來源是何種語言。

二、無關性的實現基礎

  • 各類不一樣平臺的虛擬機
  • 全部平臺都統一使用的字節碼存儲格式

2、Class 類文件的結構

Class 類文件是一組以 8 字節爲基礎單位的二進制流,各個數據項目嚴格按照順序緊湊地排列在 Class 文件中,中間沒有添加任何分隔符。當遇到須要佔用 8 字節以上空間的數據項目時,則按照高位在前(最高位字節在地址最低位)的方式分割成若干個 8 位字節進行存儲。數組

Class 文件格式採用一種相似 C 語言結構體的僞結構來存儲數據,這種僞結構中只有兩種數據類型:無符號數和表。安全

  • 無符號數: 基本數據類型,以 u一、u二、u四、u8 來分別表明 1 個字節、2 個字節、4 個字節和 8 個字節的無符號數。可用來描述數字、索引引用、數量值或按照 UTF-8 編碼構成字符串值。
  • 表: 由多個無符號數或其餘表做爲數據項構成的複合數據類型,全部表都習慣性地以「_info」結尾。表用於描述有層次關係的複合結構數據,整個 Class 文件本質上就是一張表。

不管是無符號數仍是表,當須要描述同一類型但數量不定的多個數據時,會使用一個前置的容量計數器加若干個連續數據項的形式,這若干個連續數據項稱爲集合架構

Class 文件格式:ide

類型 名稱 數量
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

一、魔數

每一個 Class 文件的頭 4 個字節稱爲魔數,用於肯定該文件是否爲一個能被虛擬機接受的 Class 文件。其值爲:0xCAFEBABE(咖啡寶貝?)。this

二、Class 文件的版本

緊接着魔數的 4 個字節存儲的是 Class 文件的版本號:第 五、6 個字節是次版本號,第 七、8 個字節是主版本號。編碼

三、常量池

緊接着主次版本號以後的是常量池入口,常量池能夠理解爲 Class 文件中的資源倉庫。線程

因爲常量池中常量的數量是不固定的,因此在常量池入口放置了一個 u2 類型的常量池容量計數器。該計數器的索引值是從 1 而不是從 0 開始,當表示「不引用任何一個常量池項目」時,則可將計數器置爲 0。code

常量池主要存放兩大類常量:字面量和符號引用。每一項常量都是一個表,這些表開始的第一位是一個 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)...

四、訪問標誌

常量池以後,緊接着的兩個字節表明訪問標誌,用於識別一些類或接口層次的訪問信息,包括:這個 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 標識這是一個枚舉

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

類索引和父類索引都是 u2 類型的數據,而接口索引集合是一組 u2 類型的數據的集合,Class 文件由這三項數據肯定這個類的繼承關係。

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

六、字段表集合

字段表用於描述接口或類中聲明的變量。字段包括類級變量以及實例變量,但不包括在方法內部聲明的局部變量。

字段表結構:

類型 名稱 數量
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):用於存儲一些額外的信息。

七、方法表集合

方法表的結構與字段表同樣,依次包括了訪問標誌(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」的屬性裏面。

八、屬性表集合

在 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)...

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。

一、加載和存儲指令

加載和存儲指令用於將數據在棧幀中的局部變量表和操做數棧之間來回傳輸,這類指令包括:

  • 將一個局部變量加載到操做棧: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 指令。

二、運算指令

運算或算術指令用於對兩個操做數以上的值進行某種特定運算,並把結果從新存入到操做數棧頂。大致上算術指令可分爲兩種:對整型數據進行運算的指令和對浮點型數據進行運算的指令。全部的算術指令以下:

  • 加法指令: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。

三、類型轉換指令

類型轉換指令能夠將兩種不一樣的數值類型進行相互轉換,這些轉換操做通常用於實現用戶代碼中的顯式類型轉換操做,或者用來處理字節碼指令集中數據類型相關指令沒法與數據類型一一對應的問題。

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

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

相對的,處理窄化類型轉換時,必須顯示地使用轉換指令來完成,這些指令包括:i2b、i2c、i2s、l2i、f2i、f2l、d2i、d2l、d2f。窄化類型轉換可能會致使轉換結果產生不一樣的正負號、不一樣的數量級的狀況,轉換過程極可能會致使數值的精度丟失。

四、對象建立與訪問指令

雖然類實例和數組都是對象,但 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。

五、操做數棧管理指令

Java 虛擬機提供了一些用於直接操做操做數棧的指令,包括:

  • 將操做數棧的棧頂一個或兩個元素出棧:pop、pop2。
  • 複製棧頂一個或兩個數值並將複製值或雙份的複製值從新壓入棧頂:dup、dup二、dup_x一、dup2_x一、dup_x二、dup2_x2。
  • 將棧最頂端的兩個數值互換:swap。

六、控制轉移指令

控制轉移指令可讓 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。

七、方法調用和返回指令

方法調用指令與數據類型無關,包括:

  • invokevirtual 指令:用於調用對象的實例方法,根據對象的實際類型進行分派(虛方法分派),這是 Java 中最多見的方法分派方式。
  • invokeinterface 指令:用於調用接口方法,它會在運行時搜索一個實現了這個接口方法的對象,找出合適的方法進行調用。
  • invokespecial 指令:用於調用一些須要特殊處理的實例方法,包括實例初始化方法、私有方法和父類方法。
  • invokestatic 指令:用於調用類方法。
  • invokedynamic 指令:用於在運行時動態解析出調用點限定符所引用的方法,並執行該方法。前 4 條調用指令的分派邏輯固化在 Java 虛擬機內部,而 invokedynamic 指令的分派邏輯是由用戶所設定的引導方法決定的。

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

八、異常處理指令

Java 虛擬機中顯式拋出異常的操做(throw 語句)都由 athrow 指令實現。而處理異常(catch 語句)則不是由字節碼指令來實現的(好久以前曾經使用 jsr 和 ret 指令實現),而是採用異常表來完成。

九、同步指令

Java 虛擬機能夠支持方法級的同步和方法內部一段指令序列的同步,這兩種同步結構都是使用管程(Monitor)來支持的。

方法級的同步是隱式的,即無須經過字節碼指令來控制。虛擬機能夠從方法訪問標誌 ACC_SYNCHRONIZED 得知一個方法是否聲明爲同步方法。若是方法訪問標誌 ACC_SYNCHRONIZED 被設置爲 true,執行線程就要求先成功持有管程,而後才能執行方法,最後當方法完成時釋放管程。

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

相關文章
相關標籤/搜索