JVM学习-Class文件结构
文章原文:https://gaoyubo.cn/blogs/844dc0e7.html
一、Class類文件的結構
任何一個Class文件都對應著唯一的一個類或接口的定義信息。
但是反過來說,類或接口并不一定都得定義在文件里(譬如類或接口也可以動態生成,直接送入類加載器中)。
Class 文件是一組以 8 位字節為基礎單位的二進制流,各個數據項目嚴格按照順序緊湊地排列在 Class 文件中,中間沒有任何分隔符。
Java 虛擬機規范規定 Class 文件采用一種類似 C 語言結構體的偽結構來存儲數據,這種偽結構中只有兩種數據類型:無符號數和表。
- 無符號數: 無符號數屬于基本數據類型,以 u1、u2、u4、u8 分別代表 1 個字節、2 個字節、4 個字節和 8 個字節的無符號數,可以用它來描述數字、索引引用、數量值或 utf-8 編碼的字符串值。
- 
表: 表是由多個無符號數或其他表為數據項構成的復合數據類型,名稱上都以 _info結尾。
整個Class文件本質上也可以視作是一張表,這張表由數據項按嚴格順序排列構成
| 英文名稱 | 中文名稱 | 類型 | 數量 | 
|---|---|---|---|
| magic | 魔數 | u4 | 1 | 
| minor_version | 次版本號 | u2 | 1 | 
| major_version | 主版本號 | u2 | 1 | 
| constant_pool_count | 常量池計數 | u2 | 1 | 
| constant_pool | 常量池 | cp_info | constant_pool_count - 1 | 
| access_flags | 訪問標志 | u2 | 1 | 
| this_class | 類索引 | u2 | 1 | 
| super_class | 父類索引 | u2 | 1 | 
| interfaces_count | 接口計數 | u2 | 1 | 
| interfaces | 接口索引集合 | u2 | interfaces_count | 
| fields_count | 字段計數 | u2 | 1 | 
| fields | 字段表集合 | field_info | fields_count | 
| methods_count | 方法計數 | u2 | 1 | 
| methods | 方法表集合 | method_info | methods_count | 
| attributes_count | 屬性計數 | u2 | 1 | 
| attributes | 屬性集合 | attribute_info | attributes_count | 
其中,cp_info 、field_info、method_info 和 attribute_info 是更具體的結構,包含了常量池項、字段信息、方法信息和屬性信息的詳細描述。
無論是無符號數還是表,當需要描述同一類型但數量不定的多個數據時,經常會使用一個前置的容量計數器加若干個連續的數據項的形式,這時候稱這一系列連續的某一類型的數據為某一類型的“集
合”。
示例
package algorithmAnalysis;
public class JVMTest {
    private int m;
    public int inc(){
        return m+1;
    }
    public static void main(String[] args) {
        System.out.println("gaoyubo");
    }
}
1.1魔數與版本號
Class 文件的頭 8 個字節是魔數和版本號,其中頭 4 個字節是魔數,也就是 0xCAFEBABE,它可以用來確定這個文件是否為一個能被虛擬機接受的 Class 文件(這通過擴展名來識別文件類型要安全,畢竟擴展名是可以隨便修改的)。
后 4 個字節則是當前 Class 文件的版本號,其中第 5、6 個字節是次版本號,第 7、8 個字節是主版本號。
1.2常量池
從第 9 個字節開始,就是常量池的入口,常量池是 Class 文件中:
- 與其他項目關聯最多的的數據類型;
- 占用 Class 文件空間最大的數據項目;
- Class 文件中第一個出現的表類型數據項目。
常量池的前兩個字節,即第 9、10 個字節,存放著一個 u2 類型的數據,用于表示常量池中的常量數量 cpc(constant_pool_count)。
這個計數值有一個特殊之處,即它是從 1 開始而不是從 0 開始的。
舉例而言,如果cpc = 22,那么說明常量池中包含 21 個常量,它們的索引值為 1 到 21。
第 0 項常量被保留為空,以便在某些情況下表示“不引用任何常量池項目”,此時將索引值設為 0 即可。
常量池中記錄主要包括以下兩大類常量:
- 
字面量: 接近于 Java 語言層面的常量概念
- 文本字符串
- 聲明為 final 的常量值
 
- 
符號引用:以一組符號來描述所引用的目標
- 被模塊導出或開放的包(Package)
- 類和接口的全限定名(Fully Qualified Name)
- 字段的名稱和描述符(Descriptor)
- 方法的名稱和描述符
- 方法句柄和方法類型(Method Handle、Method Type、Invoke Dynamic)
- 動態調用點動態常量(Dynamically-Computed Call Site、Dynamically-Computed Constant)
 
| 常量類型 | 標志 | 描述 | 
|---|---|---|
| CONSTANT_Utf8_info | 1 | UTF-8 編碼的字符串 | 
| CONSTANT_Integer_info | 3 | 整型字面量 | 
| CONSTANT_Float_info | 4 | 浮點型字面量 | 
| CONSTANT_Long_info | 5 | 長整型字面量 | 
| CONSTANT_Double_info | 6 | 雙精度浮點型字面量 | 
| CONSTANT_Class_info | 7 | 類或接口的符號引用 | 
| CONSTANT_String_info | 8 | 字符串字面量 | 
| CONSTANT_Fieldref_info | 9 | 字段的符號引用 | 
| CONSTANT_Methodref_info | 10 | 類或接口方法的符號引用 | 
| CONSTANT_InterfaceMethodref_info | 11 | 接口方法的符號引用 | 
| CONSTANT_NameAndType_info | 12 | 字段或方法的名稱和描述符 | 
| CONSTANT_MethodHandle_info | 15 | 方法句柄 | 
| CONSTANT_MethodType_info | 16 | 方法類型 | 
| CONSTANT_Dynamic_info | 17 | 動態計數常量 | 
| CONSTANT_InvokeDynamic_info | 18 | 動態方法調用點 | 
| CONSTANT_Module_info | 19 | 模塊信息 | 
| CONSTANT_Package_info | 20 | 包信息 | 
CONSTANT_Class_info
... [ tag=7 ] [ name_index ] ...
... [  1位  ] [     2位    ] ...
- tag 是標志位,用來區分常量類型的,tag = 7 就表示接下來的這個表是一個 CONSTANT_Class_info。
- name_index 是一個索引值,指向常量池中的一個 CONSTANT_Utf8_info 類型的常量所在的索引值,CONSTANT_Utf8_info 類型常量一般被用來描述類的全限定名、方法名和字段名。它的存儲結構如下:
... [ tag=1 ] [ 當前常量的長度 len ] [ 常量的符號引用的字符串值 ] ...
... [  1位  ] [        2位        ] [         len位         ] ...
CONSTANT_Fieldref_info
| 類型 | 名稱 | 數量 | 
|---|---|---|
| ul | tag | 1 | 
| u2 | class_index | 1 | 
| u2 | name_and_type_index | 1 | 
- tag: 表示標簽,值為CONSTANT_Fieldref(9)。
- class_index: 是一個指向CONSTANT_Class_info表的索引,該表中存儲了字段所屬的類或接口。
- name_and_type_index: 是一個指向CONSTANT_NameAndType_info表的索引,該表中存儲了字段的名稱和描述符。
CONSTANT_Method ref_into
以下是對固定長度的CONSTANT_Methodref_info表使用符號引用來表示類中聲明的方法(不包括接口中的方法)進行優化和潤色后的描述:固定長度的CONSTANT_Methodref_info表使用符號引用來表示類中聲明的方法(不包括接口中的方法)。
| 類型 | 名稱 | 數量 | 
|---|---|---|
| ul | tag | 1 | 
| u2 | class_index | 1 | 
| u2 | name_and_type_index | 1 | 
- 
tag(標簽):tag項的值為CONSTANT_Methodref (10)。 
- 
class_index(類索引):class_index項給出了聲明了被引用方法的類的CONSTANT_Class_info表的索引。class_index所指定的CONSTANT_Class_info表必須表示一個類,而不能是接口。指向接口中聲明的方法的符號引用應使用CONSTANT_InterfaceMethodref表。 
- 
name_and_type_index(名稱和類型索引):name_and_type_index提供了CONSTANT_NameAndType_info表的索引,該表提供了方法的簡單名稱和描述符。如果方法的簡單名稱以"<"(\u003c)符號開頭,則該方法必須是一個實例化方法。它的簡單名稱應為"",并且返回類型必須為void。否則,該方法應該是一個常規方法。 
CONSTANT_String_info
尚定長度的CONSTANT_String_info表用于存儲文字字符串值,這些值可以表示為java.lang.String類的實例。該表僅存儲文字字符串值,不存儲符號引用。
| 類型 | 名稱 | 數量 | 
|---|---|---|
| ul | tag | 1 | 
| u2 | string_index | 1 | 
- tag: 表示標簽,值為CONSTANT_String(8)。
- string_index: 是一個指向CONSTANT_Utf8_info表的索引,該表中存儲了實際的字符串值。通過使用這樣的表形式,可以方便地存儲和引用字符串值,保證了程序的靈活性和可讀性。
如果全部介紹,篇幅太長,這里使用IDEA的jclasslib插件,查看效果如下:
常量表中常量項定義如下:
1.3訪問標志
在常量池結束之后,緊接著的2個字節代表訪問標志(access_flags),這個標志用于識別一些類或者接口層次的訪問信息,包括:
這個Class是類還是接口?
- 接口:
- 是否定義為public類型;
- 是否定義為abstract類型;
 
- 類:
- 是否被聲明為final;
 
以下為訪問標志定義:
| 標志名稱 | 標志值 | 含義 | 
|---|---|---|
| ACC_PUBLIC | 0x0001 | 類或接口是公共的 | 
| ACC_FINAL | 0x0010 | 類不能被繼承;方法不能被重寫 | 
| ACC_SUPER | 0x0020 | 當用 invokespecial指令調用超類構造方法時,要求對該方法的調用使用super關鍵字 | 
| ACC_INTERFACE | 0x0200 | 標記接口 | 
| ACC_ABSTRACT | 0x0400 | 類沒有實現所有的接口方法 | 
| ACC_SYNTHETIC | 0x1000 | 標記為由編譯器生成的類或方法 | 
| ACC_ANNOTATION | 0x2000 | 標記為注解類型 | 
| ACC_ENUM | 0x4000 | 標記為枚舉類型 | 
| ACC_MODULE | 0x8000 | 標記為模塊 | 
訪問標識通常是通過按位或運算符(|)進行計算的。每個訪問標識都對應一個二進制位,通過將需要的標識的二進制位進行按位或運算,可以組合多個標識。
上文的JVMTest.java:它的訪問標識應該是
ACC_PUBLIC和ACC_SUPER。以下是分析:
ACC_PUBLIC(0x0001): 這個標志表示類是公共的,可以從其他包訪問。
ACC_SUPER(0x0020): 在 Java 5 之前,這個標志是為了向后兼容,當使用invokespecial指令調用超類構造方法時,要求對該方法的調用使用super關鍵字。因此,
JVMTest類的訪問標識應該是ACC_PUBLIC | ACC_SUPER,即 0x0021。
1.4類索引、父類索引與接口索引
類索引(this_class)和父類索引(super_class)
- 
類型:
- 
this_class和super_class都是u2類型的數據。
 
- 
- 
作用:
- 
this_class用于確定這個類的全限定名。
- 
super_class用于確定這個類的父類的全限定名。
 
- 
- 
繼承關系:
- 由于 Java 不允許多重繼承,父類索引只有一個。
- 除了 java.lang.Object之外,所有 Java 類都有父類。
- 所以,除了 java.lang.Object外,所有 Java 類的父類索引都不為 0。
 
- 
索引值:
- 類索引(this_class)和父類索引(super_class)分別用兩個u2類型的索引值表示。
- 這兩個索引值分別指向一個類型為 CONSTANT_Class_info的類描述符常量。
 
- 類索引(
- 
全限定名查找:
- 通過 CONSTANT_Class_info類型的常量中的索引值,可以找到定義在CONSTANT_Utf8_info類型的常量中的全限定名字符串。
 
- 通過 
接口索引集合(interfaces)
- 
類型:
- 
interfaces是一組u2類型的數據的集合。
 
- 
- 
作用:
- 用于描述這個類實現了哪些接口。
 
- 
排列順序:
- 接口索引集合中的接口將按 implements關鍵字后的接口順序從左到右排列。
 
- 接口索引集合中的接口將按 
- 
注意事項:
- 如果這個 Class 文件表示的是一個接口,則應當使用 extends關鍵字。
 
- 如果這個 Class 文件表示的是一個接口,則應當使用 
通過這三項數據,可以建立起類的繼承關系和接口實現關系,確定類的層次結構和實現的接口,如下為全限定名索引查找過程。
class文件中示例
訪問標志后面緊跟類索引、父類索引、接口索引,JVMTest.class中表示如下,這里類索引u2值為0x0005,父類索引u2值為0x0006:
使用jclasslib查看u2值對應常量如下,可以看出JVMTest類的父類為Object類:
1.5字段表集合
- 
描述:
- 
field_info用于描述接口或類中聲明的字段(變量)。
- 字段包括類級變量和實例級變量,但不包括在方法內部聲明的局部變量。
 
- 
- 
字段信息包含的修飾符:
- 
作用域修飾符: 可以是 public、private、protected。
- 
變量類型修飾符: 區分實例變量和類變量,使用 static修飾符。
- 
可變性修飾符: 使用 final修飾符。
- 
并發可見性修飾符: 使用 volatile修飾符,表示是否強制從主內存讀寫。
- 
序列化修飾符: 使用 transient修飾符,表示是否可被序列化。
 
- 
作用域修飾符: 可以是 
- 
字段數據類型:
- 包括基本類型、對象和數組等。
- 數據類型不固定,通過引用常量池中的常量來描述。
 
- 
字段名稱:
- 字段名稱不固定,通過引用常量池中的常量來描述。
 
- 
修飾符的表示:
- 修飾符都是布爾值,要么存在某個修飾符,要么不存在。
- 使用標志位來表示修飾符的存在與否,以便緊湊地表示多個修飾符。
 
通過 field_info,可以詳細描述字段的各種屬性和特征,為 Java 類或接口的字段提供了靈活而精確的定義。
因此字段表結構定義如下:
| 名稱 | 類型 | 描述 | 數量 | 
|---|---|---|---|
| access_flags | u2 | 訪問標志 | 1 | 
| name_index | u2 | 字段名索引 | 1 | 
| descriptor_index | u2 | 描述符索引 | 1 | 
| attributes_count | u2 | 屬性計數 | 1 | 
| attributes | attribute_info | 屬性集合 | attributes_count | 
字段表訪問標志(access_flags)
其中,access_flags字段訪問標志定義如下:
| 名稱 | 標志值 | 描述 | 
|---|---|---|
| ACC_PUBLIC | 0x0001 | 公共訪問標志 | 
| ACC_PRIVATE | 0x0002 | 私有訪問標志 | 
| ACC_PROTECTED | 0x0004 | 受保護訪問標志 | 
| ACC_STATIC | 0x0008 | 靜態字段標志 | 
| ACC_FINAL | 0x0010 | 常量字段標志 | 
| ACC_VOLATILE | 0x0040 | 可變字段標志(并發可見性) | 
| ACC_TRANSIENT | 0x0080 | 短暫字段標志(不可序列化) | 
| ACC_SYNTHETIC | 0x1000 | 由編譯器自動產生的標志 | 
| ACC_ENUM | 0x4000 | 枚舉類型字段標志 | 
簡單描述和描述符(name_index和descriptor_index)
跟隨access_flags標志的是兩項索引值:name_index和descriptor_index。
- 這兩個索引值緊隨 access_flags標志之后,分別引用常量池中的項。
- 
name_index代表字段的簡單名稱,指向常量池中的字符串項。
- 
descriptor_index代表字段和方法的描述符,同樣指向常量池中的字符串項。
全限定名: 類似于
org/fenixsoft/clazz/TestClass,是類的完整名稱,將包名中的.替換為/。為了在使用時避免混淆,通常在最后加入一個分號;表示全限定名結束。
簡單名稱: 指沒有類型和參數修飾的方法或字段名稱。例如,
inc和m是inc()方法和m字段的簡單名稱。
描述符:
- 描述符用于描述字段的數據類型、方法的參數列表(包括數量、類型和順序)以及返回值。
- 基本數據類型(
byte、char、double、float、int、long、short、boolean)以及代表無返回值的void類型都用一個大寫字符表示。- 對象類型則用字符
L加對象的全限定名表示。
如下為描述符的定義
| 標識字符 | 含義 | 
|---|---|
| B | byte | 
| C | char | 
| D | double | 
| F | float | 
| I | int | 
| J | long | 
| S | short | 
| Z | boolean | 
| V | void | 
| L | 對象類型(類或接口),如 Ljava/lang/Object | 
| [ | 數組類型,可以嵌套, java.lang.String[][]類型的二維數組將被記錄成[[Ljava/lang/String一個整型數組 int[]將被記錄成[I | 
方法描述符按照先參數列表、后返回值的順序描述,參數列表按照參數的嚴格順序放在一組小括號 () 之內。
- 
無參數、無返回值的方法(如 void inc()):- 描述符為 ()V。
 
- 描述符為 
- 
有返回值的方法(如 java.lang.String toString()):- 描述符為 ()Ljava/lang/String;。
- 參數列表為空,返回值為對象類型(Ljava/lang/String;)。
 
- 描述符為 
- 
有多個參數和返回值的方法(如 int indexOf(char[] source, int sourceOffset, int sourceCount, char[] target, int targetOffset, int targetCount, int fromIndex)):- 描述符為 ([CII[CIII)I。
- 參數列表:
- 
([C:char 數組類型
- 
II:兩個 int 類型
- 
[C:另一個 char 數組類型
- 
III:三個 int 類型
 
- 
- 返回值:I表示 int 類型。
 
- 描述符為 
屬性表
字段表中的固定數據項一直到 descriptor_index 為止,而在 descriptor_index 之后,跟隨著一個屬性表集合。這個屬性表集合用于存儲一些額外的信息,允許字段表附加描述零至多項的額外信息。
- 
屬性表計數器:
- 用于記錄附加到字段上的屬性個數。
- 計數器的值決定了接下來有多少個屬性項。
 
- 
屬性表中可能的額外信息:
- ConstantValue 屬性:
- 如果字段被聲明為 final static int m = 123;,則可能存在一項名稱為ConstantValue的屬性。
- 這個屬性的值指向常量 123。
 
- 如果字段被聲明為 
 
- ConstantValue 屬性:
- 
其他屬性項:
- 根據字段的具體聲明,可能存在其他類型的屬性,如訪問控制等。
 
通過屬性表集合,字段表可以攜帶額外的信息,例如常量值、訪問控制等,以滿足不同字段的需求。在本例中,由于字段 m 的聲明為 final static int m = 123;,因此可能包含 ConstantValue 屬性,指向常量 123。
字段表集合的特性
- 
不包含從父類或父接口中繼承的字段:
- 字段表集合中不會列出從父類或者父接口中繼承而來的字段。
- 繼承的字段在子類的字段表中不會重復出現,因為已經在父類的字段表中定義。
 
- 
可能包含編譯器生成的字段:
- 在某些情況下,編譯器會自動添加一些字段,例如在內部類中為了保持對外部類的訪問性,可能會自動添加指向外部類實例的字段。
 
- 
字段重名的合法性:
- 在 Java 語言中,字段是無法重載的,即兩個字段的數據類型、修飾符不管是否相同,都必須使用不同的名稱。
- 但在 Class 文件格式中,只要兩個字段的描述符不是完全相同,字段重名是合法的。描述符不同即使字段名稱相同也是合法的。
 
class文件中示例
在class文件中,表示如下,按照順序分別是fields_count,access_flags,name_index,descriptor_index:
0x0001:說明這個類只有一個字段表數據
0x0002:代表private修飾符的ACC_PRIVATE 標志位為真(ACC_PRIVATE標志的值為0x0002)
0x0008:字面量為m,在常量池中對應內容如下圖
0x0009:字面量I,在常量池中對應內容如下圖
與類訪問標志相同,字段訪問標志計算字段訪問標志的值也是通過按位或(
|)操作將各個標志的值組合而成的。例如,如果一個字段是
public和static的,那么其訪問標志的值為ACC_PUBLIC | ACC_STATIC如果有兩個字段,那么這個順序就會重復兩次,依次表示兩個字段的描述信息。
1.6方法表集合
Class文件存儲 格式中對方法的描述與對字段的描述采用了幾乎完全一致的方式,方法表的結構如同字段表一樣。
依次包括訪問標志(access_flags)、名稱索引(name_index)、描述符索引(descriptor_index)、屬性表集合(attributes)幾項
因此方法表表結構定義如下:
| 名稱 | 類型 | 描述 | 數量 | 
|---|---|---|---|
| access_flags | u2 | 訪問標志 | 1 | 
| name_index | u2 | 方法名索引 | 1 | 
| descriptor_index | u2 | 描述符索引 | 1 | 
| attributes_count | u2 | 屬性計數 | 1 | 
| attributes | attribute_info | 屬性集合 | attributes_count | 
方法的定義可以通過訪問標志、名稱索引、描述符索引來表達清楚,但方法內部的Java代碼去哪里了?
方法內的Java代碼在經過Javac編譯器編譯成字節碼指令后,實際上存放在方法屬性表集合中的一個名為“Code”的屬性里面。屬性表作為Class文件格式中最具擴展性的一種數據項目,將在后續介紹。
方法表的訪問標志
方法表的訪問標志中不包含 ACC_VOLATILE 和 ACC_TRANSIENT 標志,因為 volatile 和 transient 關鍵字不能修飾方法。
相反,方法表的訪問標志中增加了以下標志,因為這些關鍵字可以修飾方法:
- 
ACC_SYNCHRONIZED:用于修飾同步方法,表示該方法是同步方法。
- 
ACC_NATIVE:表示該方法用其他語言(如 C)實現,由本地方法庫提供。
- 
ACC_STRICTFP:表示該方法遵循 IEEE 754 浮點運算規范。
- 
ACC_ABSTRACT:表示該方法是抽象方法,沒有具體的實現。
以下是方法表的訪問標志及其取值:
| 標志名稱 | 標志值 | 描述 | 
|---|---|---|
| ACC_PUBLIC | 0x0001 | 公共訪問標志 | 
| ACC_PRIVATE | 0x0002 | 私有訪問標志 | 
| ACC_PROTECTED | 0x0004 | 受保護訪問標志 | 
| ACC_STATIC | 0x0008 | 靜態方法標志 | 
| ACC_FINAL | 0x0010 | 常量方法標志 | 
| ACC_SYNCHRONIZED | 0x0020 | 同步方法標志 | 
| ACC_BRIDGE | 0x0040 | 橋接方法標志 | 
| ACC_VARARGS | 0x0080 | 可變參數方法標志 | 
| ACC_NATIVE | 0x0100 | 本地方法標志 | 
| ACC_ABSTRACT | 0x0400 | 抽象方法標志 | 
| ACC_STRICTFP | 0x0800 | 嚴格浮點標志 | 
| ACC_SYNTHETIC | 0x1000 | 由編譯器自動生成的標志 | 
class文件中示例
按照順序分別為:method_count,access_flags,name_index,descriptor_index,attributes_count,attribute_name_index
0x0003(method_count):說明這個類有三個方法,編譯器自動添加了<init>方法,即實例構造器,如下:
0x0001(access_flags):只有ACC_PUBLIC標志為真
0x000A(name_index):字面量索引位10:字面量為<init>
0x000B(descriptor_index):字面量索引位11,字面量()V,代表void返回類型,參數列表為空
0x0001(attributes_count):表示此方法的屬性表集合有1項屬性
0x000C(attribute_name_index):屬性名稱的索引值為0x000C,對應常量為“Code”
字段表集合相對應地,如果父類方法在子類中沒有被重寫(Override),方法表集合中就不會出 現來自父類的方法信息。
但同樣地,有可能會出現由編譯器自動添加的方法,最常見的便是類構造器<clinit>()方法和實例構造器<init>()方法
在Java語言中,要重載一個方法,除了要與原方法具有相同的簡單名稱之外,還要求必須擁有一個與原方法不同的
特征簽名(Java代碼的方法特征簽名只包括方法名稱、參數順序及參數類型,而字節碼的特征簽名還包括方法返回值以及受查異常表)。
由于返回值不包含在特征簽名中,因此無法僅僅通過返回值的不同來對一個已有方法進行重載,如下圖。
然而,在Class文件格式中,特征簽名的范圍明顯更大。只要兩個方法的描述符不完全相同,它們就可以在同一個Class文件中合法共存。具體來說,如果兩個方法具有相同的名稱和特征簽名,但返回值不同,它們仍然可以在同一個Class文件中存在。
1.7屬性表集合
屬性表(attribute_info)在前面的講解之中已經出現過數次,Class文件、字段表、方法表都可以 攜帶自己的屬性表集合,以描述某些場景專有的信息。
在《Java虛擬機規范》的Java SE 12版本中,預定義屬性已經增加到29項,如下:
對于每一個屬性,它的名稱都要從常量池中引用一個CONSTANT_Utf8_info類型的常量來表示, 而屬性值的結構則是完全自定義的,只需要通過一個u4的長度屬性去說明屬性值所占用的位數即可。一個符合規則的屬性表應該滿足下表結構。
| 名稱 | 類型 | 數量 | 
|---|---|---|
| attribute_name_index | u2 | 1 | 
| attribute_length | u4 | 1 | 
| info | u1 | attribute_count | 
Code屬性
在Java程序中,方法體內的代碼在經過Javac編譯器處理之后,最終被轉化為字節碼指令,并存儲在方法表的屬性集合中的Code屬性內。需要注意的是,并非所有的方法表都必須包含Code屬性。例如,在接口或抽象類中的方法就不存在Code屬性。
| 屬性名稱 | 類型 | 描述 | 數量 | 
|---|---|---|---|
| attribute_name_index | u2 | 指向UTF-8常量的索引,表示屬性名稱(Code) | 1 | 
| max_stack | u2 | 操作數棧的最大深度 | 1 | 
| max_locals | u2 | 局部變量表的最大容量 | 1 | 
| code_length | u4 | 字節碼指令的長度 | 1 | 
| code | u1[code_length] | 存儲實際字節碼指令的數組 | code_length | 
| exception_table_length | u2 | 異常處理表的長度 | 1 | 
| exception_table | exception_info | 異常處理表 | 0或多 | 
| attributes_count | u2 | Code屬性的屬性數量 | 1 | 
| attributes | attribute_info[attributes_count] | Code屬性的屬性集合 | 0或多 | 
- max_stack: 操作數棧的最大深度,在方法執行的任意時刻,操作數棧都不會超過這個深度。
- max_locals: 局部變量表所需的存儲空間,以變量槽為單位,變量槽是虛擬機為局部變量分配內存的最小單位。
- code_length: 字節碼指令的長度,限制為不超過65535字節。
- code: 存儲實際字節碼指令的一系列字節流。
- exception_table_length: 異常處理表的長度,記錄方法中的異常處理信息。
- exception_table: 異常處理表,包括起始字節碼指令位置、結束字節碼指令位置、異常處理程序入口位置和捕獲異常的類索引。
- attributes_count: Code屬性的屬性數量,用于存儲額外的屬性信息。
- attributes: Code屬性的屬性集合,可能包含一些額外的信息,如調試信息等。
class文件中示例
屬性表的attribute_name_index后的00 00 00 2F表示屬性值的長度。在這里,00 00 00 2F表示長度為47個字節。它告訴虛擬機在讀取屬性值時要讀取47個字節的內容。如果前面的0x000C的字面量Code虛擬機不認識,那么就可以跳過這些長度。
《Java虛擬機規范》允許只要不與已有屬性名重復,任何人實現的編譯器都可以向屬性表中寫入自己定義的屬性信息,Java虛擬機運行時會忽略掉它不認識的屬性。
按順序分別為:max_stack,max_locals,code_length,code
0x0001: 操作數棧的最大深度為1
0x0001: 本地變量表容量為1
0x00000005: 字節碼區域 所占空間的長度為0x0005。虛擬機讀取到字節碼區域的長度后,按照順序依次讀入緊隨的5個字節,并
根據字節碼指令表翻譯出所對應的字節碼指令
翻譯“2A B7000A B1”的過程為:
- 讀入 2A,查表得到aload_0指令,作用是將第 0 個變量槽中的reference類型的本地變量推送到操作數棧頂。
- 讀入 B7,查表得到invokespecial指令,該指令以棧頂的reference類型的數據所指向的對象作為方法接收者,調用此對象的實例構造器方法、private方法或者它的父類的方法。該方法有一個u2類型的參數,指向常量池中的一個CONSTANT_Methodref_info類型常量,即此方法的符號引用。
- 讀入 000A,這是invokespecial指令的參數,代表一個符號引用。查常量池得到0x000A對應的常量,表示實例構造器<init>()方法的符號引用。
- 讀入 B1,查表得到return指令,含義是從方法返回,并且返回值為void。執行這條指令后,當前方法正常結束。
這里查的表是 Java 虛擬機規范中定義的字節碼指令表。字節碼指令表包含了每個操作碼(opcode)對應的具體指令和操作。
部分其他指令如下:
指令 助記符 描述 0x03 iconst_2 將整數常量值 2 推送到操作數棧頂 0x10 bipush 將一個字節推送到棧頂,作為整數使用 0x60 iadd 將棧頂兩個整數相加 0x2D fsub 將棧頂兩個浮點數相減 0xC7 ifnonnull 如果引用不為 null,則跳轉 
異常表
在字節碼指令之后的是這個方法的顯式異常處理表(下文簡稱“異常表”)集合,異常表對于Code 屬性來說并不是必須存在的。
異常表的格式如下:
| 字段名 | 數據類型 | 描述 | 
|---|---|---|
| start_pc | u2 | 起始字節碼行號 | 
| end_pc | u2 | 結束字節碼行號(不含) | 
| handler_pc | u2 | 異常處理代碼的字節碼行號 | 
| catch_type | u2 | 指向一個CONSTANT_Class_info型常量的索引,表示捕獲的異常類型。為0時表示捕獲所有異常。 | 
演示:
public int inc() {
    int x;
    try {
        x = 1;
        return x;
    } catch (Exception e) {
        x = 2;
        return x;
    } finally {
        x = 3;
    }
}
編譯后的字節碼和異常表:
public int inc(); 
Code: 
  Stack=1, Locals=5, Args_size=1 
  0: iconst_1      // 將整數1推送到棧頂,try塊中的x=1
  1: istore_1      // 將棧頂的值存儲到本地變量表的變量槽1中
  2: iload_1       // 將本地變量表中的變量槽1的值推送到棧頂
  3: istore 4      // 將棧頂的值存儲到本地變量表的變量槽4中
  5: iconst_3      // 將整數3推送到棧頂,finally塊中的x=3
  6: istore_1      // 將棧頂的值存儲到本地變量表的變量槽1中
  7: iload 4       // 將本地變量表中的變量槽4的值推送到棧頂
  9: ireturn       // 從方法返回,返回值為棧頂的值
10: astore_2       // 將棧頂的異常對象存儲到本地變量表的變量槽2中
11: iconst_2       // 將整數2推送到棧頂,catch塊中的x=2
12: istore_1       // 將棧頂的值存儲到本地變量表的變量槽1中
13: iload_1        // 將本地變量表中的變量槽1的值推送到棧頂
14: istore 4       // 將棧頂的值存儲到本地變量表的變量槽4中
16: iconst_3       // 將整數3推送到棧頂,finally塊中的x=3
17: istore_1       // 將棧頂的值存儲到本地變量表的變量槽1中
18: iload 4        // 將本地變量表中的變量槽4的值推送到棧頂
20: ireturn        // 從方法返回,返回值為棧頂的值
21: astore_3       // 將棧頂的異常對象存儲到本地變量表的變量槽3中
22: iconst_3       // 將整數3推送到棧頂,finally塊中的x=3
23: istore_1       // 將棧頂的值存儲到本地變量表的變量槽1中
24: aload_3        // 將本地變量表中的變量槽3的值(異常對象)推送到棧頂
25: athrow         // 拋出棧頂的異常
Exception table: 
  from    to  target type
     0     0    10   Class java/lang/Exception
     5     5    16   any
    10    21    21   Class java/lang/Exception
在這段字節碼中,前五行主要是try塊的內容。首先,整數1被賦給變量x,然后通過istore_1指令將x的值保存在第一個本地變量槽(slot)中。接下來,將3推送到操作數棧,再通過istore指令將其存儲在第四個本地變量槽中,這個槽被稱為returnValue。
接下來的iload_1指令將第一個本地變量槽中的x值加載到操作數棧頂,然后通過ireturn指令返回這個值。因此,如果try塊中沒有異常,方法將返回1。
在異常情況下,程序將跳轉到第10行(catch塊)。異常處理塊首先將2賦給變量x,然后通過istore_1指令將x的值保存在第一個本地變量槽中。接著,將之前保存在returnValue中的值(即1)加載到操作數棧頂,然后通過ireturn指令返回這個值。因此,如果發生異常,方法將返回2。
最后,無論是否發生異常,程序都會執行finally塊(第21行開始)。在finally塊中,將3賦給變量x,并使用athrow指令拋出之前發生的異常。雖然這里沒有具體的異常類型,但finally塊的主要目的是在方法返回前執行清理工作。
Exceptions屬性
這里的Exceptions屬性是在方法表中與Code屬性平級的一項屬性,不要與前面剛剛講解完的異常表產生混淆。
Exceptions屬性的作用是列舉出方法中可能拋出的受查異常(Checked Excepitons),也就是方法描述時在throws關鍵字后面列舉的異常。
| 字段名 | 類型 | 描述 | 
|---|---|---|
| attribute_name_index | u2 | 指向常量池中CONSTANT_Utf8_info類型的異常表屬性名稱的索引 | 
| attribute_length | u4 | 屬性值的長度,不包括attribute_name_index和attribute_length自身的長度 | 
| number_of_exceptions | u2 | 異常表中的異常個數 | 
| exception_index_table | u2 數組 | 每個元素都是指向常量池中CONSTANT_Class_info類型的索引,表示受檢異常的類型 | 
LineNumberTable 屬性
LineNumberTable屬性用于描述Java源碼行號與字節碼行號(字節碼的偏移量)之間的對應關系。雖然它不是運行時必需的屬性,但默認會生成到Class文件中。通過使用Javac中的-g:none或-g:lines選項,可以選擇是否生成這項信息。如果選擇不生成LineNumberTable屬性,對程序運行的主要影響之一是在拋出異常時,堆棧跟蹤中將不會顯示出錯的行號。此外,調試程序時也無法按照源碼行來設置斷點。
在調試和排查問題時,LineNumberTable屬性是非常有用的,因為它建立了Java源代碼和編譯后的字節碼之間的映射。
| 字段名 | 類型 | 描述 | 
|---|---|---|
| attribute_name_index | u2 | 指向常量池中CONSTANT_Utf8_info類型的屬性名稱 "LineNumberTable" 的索引 | 
| attribute_length | u4 | 屬性值的長度,不包括 attribute_name_index 和 attribute_length 自身的長度 | 
| line_number_table | 表 | 包含多個行號項的表,每個行號項包括 start_pc 和 line_number 字段,表示字節碼行號和源代碼行號的映射關系 | 
LocalVariableTable 屬性
LocalVariableTable屬性用于描述棧幀中局部變量表的變量與Java源碼中定義的變量之間的關系。雖然它不是運行時必需的屬性,但默認會生成到Class文件中。可以使用Javac中的-g:none或-g:vars選項來選擇是否生成這項信息。如果沒有生成這項屬性,最大的影響之一是當其他人引用這個方法時,所有的參數名稱都將會丟失。例如,IDE將會使用諸如arg0、arg1之類的占位符代替原有的參數名。這對程序運行沒有影響,但會對代碼編寫帶來較大不便,而且在調試期間無法根據參數名稱從上下文中獲取參數值。
LocalVariableTable屬性對于理解程序的執行過程以及在調試中獲取更多有關局部變量的信息非常有用。
| 字段名 | 類型 | 描述 | 
|---|---|---|
| attribute_name_index | u2 | 指向常量池中CONSTANT_Utf8_info類型的屬性名稱 "LocalVariableTable" 的索引 | 
| attribute_length | u4 | 屬性值的長度,不包括 attribute_name_index 和 attribute_length 自身的長度 | 
| local_variable_table | 表 | 包含多個局部變量項的表,每個局部變量項包括 start_pc、length、name_index、descriptor_index 和 index 字段,表示局部變量在字節碼中的范圍、名稱、描述符和索引 | 
SourceFile 屬性
SourceFile屬性用于記錄生成這個Class文件的源碼文件名稱。這個屬性是可選的,可以使用Javac的-g:none或-g:source選項來關閉或要求生成這項信息。在大多數情況下,Java類的類名和文件名是一致的,但是在一些特殊情況(例如內部類)下可能存在例外情況。如果不生成這項屬性,當拋出異常時,堆棧中將不會顯示出錯代碼所屬的文件名。這個屬性是一個定長的屬性。
ourceFile屬性有助于在調試時追蹤代碼,特別是在涉及多個源文件的項目中。
| 字段名 | 類型 | 描述 | 
|---|---|---|
| attribute_name_index | u2 | 指向常量池中CONSTANT_Utf8_info類型的屬性名稱 "SourceFile" 的索引 | 
| attribute_length | u4 | 屬性值的長度,不包括 attribute_name_index 和 attribute_length 自身的長度 | 
| sourcefile_index | u2 | 指向常量池中CONSTANT_Utf8_info類型的源文件名的索引 | 
SourceDebugExtension 屬性
SourceDebugExtension屬性是為了存儲額外的代碼調試信息,特別是在涉及非Java語言編寫、但需要編譯成字節碼并在Java虛擬機中運行的程序時。這個屬性的數據項是指向常量池中CONSTANT_Utf8_info型常量的索引,該常量的值是源代碼文件的調試信息。
在JDK 5時,引入了SourceDebugExtension屬性,用于存儲JSR 45提案所定義的標準調試信息。這對于需要在Java虛擬機中運行的非Java語言編寫的程序提供了一種標準的調試機制。典型的場景是在進行JSP文件調試時,由于無法通過Java堆棧來定位到JSP文件的行號,可以使用SourceDebugExtension屬性來存儲額外的調試信息,使程序員能夠更快速地從異常堆棧中定位到原始JSP中出現問題的行號。
這個屬性在一些特定的情況下很有用,但在一般的Java程序開發中,由于使用Java語言編寫,通常不需要額外的非Java調試信息。因此,對于大多數Java應用,可能并不常見。
| 字段名 | 類型 | 描述 | 
|---|---|---|
| attribute_name_index | u2 | 指向常量池中CONSTANT_Utf8_info類型的屬性名稱 "SourceDebugExtension" 的索引 | 
| attribute_length | u4 | 屬性值的長度,不包括 attribute_name_index 和 attribute_length 自身的長度 | 
| debug_extension | 字節數組 | 包含調試信息的字節數組 | 
還有很多屬性如:不再贅述
- AnnotationDefault
- BootstrapMethods
- 
MethodParameters
 ...
二、字節碼指令
在Java虛擬機的指令集中,指令可以分為多個大的類別,以下是其中一些主要的指令類別:
- 
加載和存儲指令(Load and Store Instructions):
- 
aaload,aastore,baload,bastore,caload,castore,daload,dastore,faload,fastore,iaload,iastore,laload,lastore,saload,sastore, 等。
 
- 
- 
操作數棧管理指令(Stack Management Instructions):
- 
pop,pop2,dup,dup_x1,dup_x2,dup2,dup2_x1,dup2_x2,swap, 等。
 
- 
- 
數學運算指令(Arithmetic Instructions):
- 
iadd,isub,imul,idiv,irem,iinc,ladd,lsub,lmul,ldiv,lrem,fadd,fsub,fmul,fdiv,frem,dadd,dsub,dmul,ddiv,drem, 等。
 
- 
- 
類型轉換指令(Type Conversion Instructions):
- 
i2l,i2f,i2d,l2i,l2f,l2d,f2i,f2l,f2d,d2i,d2l,d2f,i2b,i2c,i2s, 等。
 
- 
- 
比較指令(Comparison Instructions):
- 
lcmp,fcmpl,fcmpg,dcmpl,dcmpg,ifcmp<cond>,<cond>,if<cond>, 等。
 
- 
- 
控制轉移指令(Control Transfer Instructions):
- 
goto,tableswitch,lookupswitch,ireturn,lreturn,freturn,dreturn,areturn,return,athrow,jsr,ret,if<cond>, 等。
 
- 
- 
引用類和對象的指令(Reference Instructions):
- 
new,newarray,anewarray,multianewarray,checkcast,instanceof,getfield,putfield,getstatic,putstatic, 等。
 
- 
- 
方法調用和返回指令(Method Invocation and Return Instructions):
- 
invokevirtual,invokespecial,invokestatic,invokeinterface,invokedynamic,return,areturn,ireturn,lreturn,freturn,dreturn, 等。
 
- 
- 
異常處理指令(Exception Handling Instructions):
- 
athrow,monitorenter,monitorexit,try-catch-finally塊相關的指令。
 
- 
這些指令構成了Java虛擬機的指令集,用于執行Java字節碼。每個指令都有特定的操作碼和操作數,用于在操作數棧上執行相應的操作
字節碼指令集在Java虛擬機中具有獨特的特點和一些限制:
- 操作碼長度限制: 指令集的操作碼被限制為一個字節,范圍為0~255,這意味著指令集的操作碼總數不能超過256條。這種設計有助于簡化指令的編碼和解碼過程。
- 
操作數長度對齊: Class文件格式中放棄了編譯后代碼的操作數長度對齊。這意味著虛擬機在處理超過一個字節的數據時,需要在運行時從字節中重建具體數據的結構。例如,將一個16位長度的無符號整數存儲在兩個無符號字節中,需要使用表達式 (byte1 << 8) | byte2進行重建。
這些設計選擇有一些優勢和劣勢:
優勢:
- 緊湊性: 一個字節的操作碼和簡化的操作數對于Class文件的緊湊性是有利的,減小了字節碼文件的大小。
- 解析速度: 簡單的指令格式和有限的操作碼范圍有助于提高字節碼的解析速度。
劣勢:
- 指令數限制: 256條操作碼的限制可能限制了指令集的豐富性,盡管在實踐中這仍然足夠支持豐富的語義。
- 運行時處理成本: 虛擬機在處理較大的數據時需要進行運行時的計算,可能增加了一些運行時的成本。
總體而言,這些設計選擇是為了在保持緊湊性和解析速度的同時,提供足夠的靈活性來支持Java虛擬機的執行需求。
如果不考慮異常處理的話,那Java虛擬機的解釋器可以使用下面這段偽代碼作為最基本的執行模 型來理解,這個執行模型雖然很簡單,但依然可以有效正確地工作
do { 
    自動計算PC寄存器的值加1; 
    根據PC寄存器指示的位置,從字節碼流中取出操作碼; 
    if (字節碼存在操作數) 
    	從字節碼流中取出操作數; 
    	執行操作碼所定義的操作;
} while (字節碼流長度 > 0);
2.1字節碼與數據類型
如下列舉了Java虛擬機所支持的與數據類型相關的字節碼指令,通過使用數據類型列所代表的特殊字符替換opcode列的指令模板中的T,就可以得到一個具體的字節碼指令。
如果在表中指令模板與數據類型兩列共同確定的格為空,則說明虛擬機不支持對這種數據類型執行這項操作。例如load指令有操作int類型的iload,但是沒有操作byte類型的同類指令。
Java虛擬機的字節碼指令集并沒有提供專門用于處理整數類型`byte`、`char`和`short`以及布爾類型(`boolean`)的指令。相反,編譯器在編譯期或運行期進行類型轉換,將這些較小的整數類型轉換為`int`類型,然后使用`int`類型的字節碼指令來進行操作。具體而言:
- 
帶符號擴展(Sign-Extend): 對于byte和short類型,編譯器會進行帶符號擴展,將它們轉換為相應的int類型。這意味著,如果原始值是負數,它會被符號擴展為32位帶符號整數。
- 
零位擴展(Zero-Extend): 對于boolean和char類型,同樣會進行零位擴展,將它們轉換為相應的int類型。這意味著,無論原始值是什么,都會被零位擴展為32位無符號整數。
在處理boolean、byte、short和char類型的數組時,也會使用對應的int類型的字節碼指令來進行操作。因此,實際上,大多數對于這些較小整數類型的操作,都是使用int類型作為運算類型來進行的。這種設計簡化了字節碼指令集,減少了復雜性。
2.2加載和存儲指令
加載和存儲指令在Java虛擬機中用于在棧幀的局部變量表和操作數棧之間傳輸數據。這些指令包括:
- 
將一個局部變量加載到操作數棧: - 
iload:將int類型的局部變量加載到操作數棧。
- 
iload_<n>:將int類型的局部變量加載到操作數棧,其中<n>表示局部變量索引,可以是0到3的數字。
 (類似的指令存在于其他數據類型,如 lload、fload、dload、aload)
- 
- 
將一個數值從操作數棧存儲到局部變量表: - 
istore:將int類型的數值存儲到局部變量表。
- 
istore_<n>:將int類型的數值存儲到局部變量表,其中<n>表示局部變量索引,可以是0到3的數字。
 (類似的指令存在于其他數據類型,如 lstore、fstore、dstore、astore)
- 
- 
將一個常量加載到操作數棧: - 
bipush:將單字節常量(-128到127之間的整數)推送到操作數棧。
- 
sipush:將短整型常量(-32768到32767之間的整數)推送到操作數棧。
- 
ldc:將int、float或String類型的常量值從常量池中推送到操作數棧。
- 
ldc_w:與ldc類似,但用于更大的常量池索引。
 (其他指令用于加載更大的常量,如 ldc2_w、aconst_null、iconst_m1、iconst_<i>、lconst_<l>、fconst_<f>、dconst_<d>)
- 
- 
擴充局部變量表的訪問索引的指令: - 
wide:用于擴大對局部變量表的訪問索引,通常與其他指令一起使用。
 
- 
一些指令的助記符以尖括號結尾,表示這是一組指令的特殊形式。例如,
iload_<n>表示了一組特殊的iload指令,其中<n>可以是0到3的數字。這些特殊指令省略了顯式的操作數,因為操作數隱含在指令中。這些指令的語義與原生的通用指令完全一致。
2.3運算指令
Java虛擬機的算術指令用于對兩個操作數棧上的值進行特定運算,并將結果重新存入操作數棧頂。主要分為對整型數據和浮點型數據的運算,其中涵蓋了加法、減法、乘法、除法、求余、取反、位移、按位或、按位與、按位異或、局部變量自增、比較等操作。
以下是具體的算術指令列表:
整數運算指令(對應不同數據類型,如int、long):
- 加法指令:iadd、ladd
- 減法指令:isub、lsub
- 乘法指令:imul、lmul
- 除法指令:idiv、ldiv
- 求余指令:irem、lrem
- 取反指令:ineg、lneg
- 位移指令:ishl、ishr、iushr、lshl、lshr、lushr
- 按位或指令:ior、lor
- 按位與指令:iand、land
- 按位異或指令:ixor、lxor
- 局部變量自增指令:iinc
- 比較指令:dcmpg、dcmpl、fcmpg、fcmpl、lcmp
浮點數運算指令(對應不同數據類型,如float、double):
- 加法指令:fadd、dadd
- 減法指令:fsub、dsub
- 乘法指令:fmul、dmul
- 除法指令:fdiv、ddiv
- 求余指令:frem、drem
- 取反指令:fneg、dneg
在整型數據溢出的情況下,虛擬機規范并未定義具體的結果,只有在除法和求余指令中當除數為零時會拋出
ArithmeticException異常。對于浮點數運算,虛擬機要求遵循IEEE 754規范,包括對非正規浮點數值和逐級下溢的運算規則。在對long類型數值進行比較時,采用帶符號的比較方式;而對浮點數值進行比較時,采用IEEE 754規范中的無信號比較方式。
如果某個操作結果沒有明確的數學定義的話, 將會使用NaN(Not a Number)值來表示。所有使用NaN值作為操作數的算術操作,結果都會返回NaN。
這些規定確保了在Java虛擬機中進行數值運算時,結果是符合預期并具有可靠性的。
2.4類型轉換指令
類型轉換指令用于將兩種不同的數值類型相互轉換,主要分為寬化類型轉換(Widening Numeric Conversion)和窄化類型轉換(Narrowing Numeric Conversion)兩種。
Java虛擬機直接支持寬化類型轉換,即將小范圍類型向大范圍類型進行安全轉換。例如:
- 將int類型轉換為long、float或double類型
- 將long類型轉換為float或double類型
- 將float類型轉換為double類型
窄化類型轉換必須顯式地使用轉換指令完成,包括:
- 
i2b:將int類型轉換為byte類型
- 
i2c:將int類型轉換為char類型
- 
i2s:將int類型轉換為short類型
- 
l2i:將long類型轉換為int類型
- 
f2i:將float類型轉換為int類型
- 
f2l:將float類型轉換為long類型
- 
d2i:將double類型轉換為int類型
- 
d2l:將double類型轉換為long類型
- 
d2f:將double類型轉換為float類型
窄化類型轉換可能導致轉換結果的正負號變化以及數值的精度丟失。在浮點數值窄化轉換為整數類型時,需遵循一定規則,如對NaN的處理和使用IEEE 754的向零舍入模式取整。虛擬機規范明確規定數值類型的窄化轉換指令不會導致運行時異常。
這些規定確保了在Java虛擬機中進行數值類型轉換時,能夠預期并具有可靠性的結果。
2.5對象創建與訪問指令
Java虛擬機對類實例和數組的創建與操作使用了不同的字節碼指令。以下是涉及對象創建和操作的一些指令:
創建類實例的指令:
- 
new:創建一個新的類實例
創建數組的指令:
- 
newarray:創建一個基本類型數組
- 
anewarray:創建一個引用類型數組
- 
multianewarray:創建一個多維數組
訪問類字段和實例字段的指令:
- 
getfield:獲取實例字段的值
- 
putfield:設置實例字段的值
- 
getstatic:獲取類字段(靜態字段)的值
- 
putstatic:設置類字段(靜態字段)的值
數組元素的加載和存儲指令:
- 
baload:將一個byte或boolean數組元素加載到操作數棧
- 
caload:將一個char數組元素加載到操作數棧
- 
saload:將一個short數組元素加載到操作數棧
- 
iaload:將一個int數組元素加載到操作數棧
- 
laload:將一個long數組元素加載到操作數棧
- 
faload:將一個float數組元素加載到操作數棧
- 
daload:將一個double數組元素加載到操作數棧
- 
aaload:將一個引用類型數組元素加載到操作數棧
- 
bastore:將一個byte或boolean值存儲到byte或boolean數組元素中
- 
castore:將一個char值存儲到char數組元素中
- 
sastore:將一個short值存儲到short數組元素中
- 
iastore:將一個int值存儲到int數組元素中
- 
lastore:將一個long值存儲到long數組元素中
- 
fastore:將一個float值存儲到float數組元素中
- 
dastore:將一個double值存儲到double數組元素中
- 
aastore:將一個引用類型值存儲到引用類型數組元素中
數組長度的指令:
- 
arraylength:獲取數組的長度
檢查類實例類型的指令:
- 
instanceof:檢查對象是否是某個類的實例
- 
checkcast:檢查對象是否可以強制轉換為指定類型
2.6操作數棧管理指令
Java虛擬機提供了一些指令,用于直接操作操作數棧。這些指令包括:
將操作數棧的棧頂一個或兩個元素出棧:
- 
pop:將棧頂一個元素彈出
- 
pop2:將棧頂兩個元素彈出
復制棧頂一個或兩個數值并將復制值或雙份的復制值重新壓入棧頂:
- 
dup:復制棧頂一個元素并將復制值重新壓入棧頂
- 
dup2:復制棧頂兩個元素并將復制值或雙份的復制值重新壓入棧頂
- 
dup_x1:復制棧頂一個元素并將復制值與棧頂下面的元素互換位置,然后重新壓入棧頂
- 
dup2_x1:復制棧頂兩個元素并將復制值或雙份的復制值與棧頂下面的元素互換位置,然后重新壓入棧頂
- 
dup_x2:復制棧頂一個元素并將復制值與棧頂下面的兩個元素互換位置,然后重新壓入棧頂
- 
dup2_x2:復制棧頂兩個元素并將復制值或雙份的復制值與棧頂下面的兩個元素互換位置,然后重新壓入棧頂
將棧最頂端的兩個數值互換:
- 
swap:將棧最頂端的兩個元素互換位置
2.7控制轉移指令
控制轉移指令在Java虛擬機中用于有條件或無條件地改變程序執行流程。這些指令包括:
條件分支:
- 
ifeq:如果棧頂元素等于0,則跳轉
- 
iflt:如果棧頂元素小于0,則跳轉
- 
ifle:如果棧頂元素小于等于0,則跳轉
- 
ifne:如果棧頂元素不等于0,則跳轉
- 
ifgt:如果棧頂元素大于0,則跳轉
- 
ifge:如果棧頂元素大于等于0,則跳轉
- 
ifnull:如果棧頂元素為null,則跳轉
- 
ifnonnull:如果棧頂元素不為null,則跳轉
- 
if_icmpeq:如果棧頂兩個int型元素相等,則跳轉
- 
if_icmpne:如果棧頂兩個int型元素不相等,則跳轉
- 
if_icmplt:如果棧頂兩個int型元素第一個小于第二個,則跳轉
- 
if_icmpgt:如果棧頂兩個int型元素第一個大于第二個,則跳轉
- 
if_icmple:如果棧頂兩個int型元素第一個小于等于第二個,則跳轉
- 
if_icmpge:如果棧頂兩個int型元素第一個大于等于第二個,則跳轉
- 
if_acmpeq:如果棧頂兩個引用類型元素相等,則跳轉
- 
if_acmpne:如果棧頂兩個引用類型元素不相等,則跳轉
復合條件分支:
- 
tableswitch:通過索引訪問表格來進行跳轉,用于switch語句的實現
- 
lookupswitch:通過鍵值對訪問表格來進行跳轉,用于switch語句的實現
無條件分支:
- 
goto:無條件跳轉
- 
goto_w:無條件跳轉(寬索引)
- 
jsr:跳轉到子例程(調用子例程)
- 
jsr_w:跳轉到子例程(調用子例程,寬索引)
- 
ret:返回子例程
寬索引是使用4個字節而不是標準的1個字節來表示跳轉目標的偏移量。這使得這兩個指令能夠處理更大范圍的代碼偏移,允許跳轉到更遠的位置。
2.8方法調用和返回指令
在Java虛擬機的指令集中,方法調用是通過一系列不同的指令完成的,這些指令涵蓋了不同類型的方法調用。以下是五個主要的方法調用指令:
- 
invokevirtual指令: - 用于調用對象的實例方法。
- 根據對象的實際類型進行分派,這是虛方法分派的典型方式。
- 是Java語言中最常見的方法分派方式。
 虛方法分派(Virtual Method Dispatch)是指在面向對象編程中,根據對象的實際類型(運行時類型)來確定調用哪個版本的方法。這種分派方式主要用于處理多態性,確保在運行時調用的是對象實際所屬類的方法,而不是編譯時所聲明的類型。 是面向對象編程中實現多態的重要機制之一。 
- 
invokeinterface指令: - 用于調用接口方法。
- 在運行時搜索一個實現了這個接口方法的對象,找出適合的方法進行調用。
 
- 
invokespecial指令: - 用于調用一些需要特殊處理的實例方法。
- 包括實例初始化方法、私有方法和父類方法。
 
- 
invokestatic指令: - 用于調用類靜態方法(static方法)。
 
- 
invokedynamic指令: - 用于在運行時動態解析出調用點限定符所引用的方法,并執行該方法。
- 與前四條調用指令不同,invokedynamic指令的分派邏輯是由用戶所設定的引導方法決定的。
 
方法調用指令與數據類型無關。方法的返回操作則根據返回值的類型有不同的指令,包括:
- 
ireturn(用于返回boolean、byte、char、short和int類型的值),
- 
lreturn(long類型的值),
- 
freturn(float類型的值),
- 
dreturn(double類型的值),
- 
areturn(引用類型的值)。
此外,還有一條return指令,供聲明為void的方法、實例初始化方法、類和接口的類初始化方法使用。
2.9異常處理指令
在Java虛擬機中,athrow 指令用于顯式拋出異常。當在程序中使用 throw 語句時,編譯器會將相應的異常對象推送到操作數棧頂,然后通過 athrow 指令將異常拋出。athrow 指令的使用類似于其他指令,只不過它專門用于拋出異常。
異常處理(catch語句)不是由特定的字節碼指令來實現的,而是通過異常表(Exception Table)來完成。異常表是一種數據結構,用于在方法的字節碼中記錄異常處理器的信息,包括受監控的范圍、捕獲的異常類型以及對應的異常處理代碼的起始位置等信息。
異常表的作用是在方法的字節碼執行過程中,當發生異常時,虛擬機會根據異常表中的信息確定如何處理異常。以下是異常表的主要結構:
- start_pc、end_pc: 定義了受監控范圍的起始和結束位置。在這個范圍內,如果發生異常,則按照異常表中的處理器信息進行處理。
- handler_pc: 指定了異常處理器的起始位置,即對應異常發生時要執行的代碼的入口。
- 
catch_type: 指定了捕獲的異常類型,是一個對常量池中CONSTANT_Class_info型常量的索引,表示捕獲的異常類型。如果catch_type的值為0,表示捕獲所有類型的異常(相當于Java中的catch(Exception e))。
異常表中的每一項都對應著一個異常處理器,Java虛擬機在發現異常時會遍歷異常表,找到第一個匹配的異常處理器,然后跳轉到相應的處理代碼塊。如果沒有找到匹配的異常處理器,那么異常將會傳遞到上層調用棧。
2.10同步指令
字節碼指令在Java虛擬機中的執行是原子性的。每個字節碼指令都被視為一個原子操作,它們要么完全執行,要么不執行。這種原子性保證了在多線程環境中,一個線程執行的字節碼指令不會被其他線程中斷或插入。
但是代碼指令則為非原子性,例如讀取和寫入共享變量。在多線程環境下,為了確保線程安全,可能需要使用額外的同步機制。
因此,Java虛擬機支持方法級的同步和方法內部一段指令序列的同步,這兩種同步結構都使用管程(Monitor,通常稱為“鎖”)來實現。
- 
方法級的同步(隱式同步): 方法級的同步是隱式的,無需通過字節碼指令控制。虛擬機可以通過檢查方法的訪問標志(ACC_SYNCHRONIZED)來確定一個方法是否被聲明為同步方法。 - 當調用同步方法時,調用指令檢查方法的訪問標志,如果設置了,執行線程要求首先成功持有管程(鎖),然后才能執行方法。最后,在方法完成時,無論是正常完成還是非正常完成,都會釋放管程。
- 在同步方法執行期間,執行線程持有管程,其他線程無法獲取相同的管程。如果同步方法執行期間拋出異常,并且在方法內部無法處理此異常,同步方法所持有的管程將在異常拋到同步方法邊界之外時自動釋放。
 
- 
同步一段指令序列: 同步一段指令序列通常由Java語言中的 synchronized語句塊表示。- Java虛擬機提供了monitorenter和monitorexit兩條指令來支持synchronized關鍵字的語義。
- 這種同步方式需要Javac編譯器與Java虛擬機共同協作來支持,舉例如下:
 void onlyMe(Foo f) { synchronized(f) { doSomething(); } }? 編譯后,這段代碼生成的字節碼序列如下: Method void onlyMe(Foo) 0 aload_1 // 將對象f入棧 1 dup // 復制棧頂元素(即f的引用) 2 astore_2 // 將棧頂元素存儲到局部變量表變量槽2中 3 monitorenter // 以棧頂元素(即f)作為鎖,開始同步 4 aload_0 // 將局部變量槽0(即this指針)的元素入棧 5 invokevirtual #5 // 調用doSomething()方法 8 aload_2 // 將局部變量槽2的元素(即f)入棧 9 monitorexit // 退出同步 10 goto 18 // 方法正常結束,跳轉到18返回 13 astore_3 // 從這步開始是異常路徑,見下面異常表的Target 14 aload_2 // 將局部變量槽2的元素(即f)入棧 15 monitorexit // 退出同步 16 aload_3 // 將局部變量槽3的元素(即異常對象)入棧 17 athrow // 把異常對象重新拋出給onlyMe()方法的調用者 18 return // 方法正常返回為了保證在方法異常完成時monitorenter和monitorexit指 令依然可以正確配對執行,編譯器會自動產生一個異常處理程序,這個異常處理程序聲明可處理所有的異常,它的目的就是用來執行monitorexit指令。 
- Java虛擬機提供了
三、公有設計、私有實現
Java虛擬機規范對于Java程序與虛擬機實現之間的關系的規定。它明確了虛擬機實現者在設計虛擬機時的*度和靈活性。一些關鍵點包括:
- 公有設計與私有實現之分界線: Java虛擬機規范定義了Java虛擬機應有的共同程序存儲格式(Class文件格式)和字節碼指令集。這些規范為Java平臺上的不同實現提供了一個通用的交互手段。規范強調了實現者可以靈活地在實現中進行優化和修改,只要保持對Class文件的正確讀取和包含在其中的語義的準確實現。
- 實現的伸縮性: 實現者可以根據虛擬機的目標和關注點選擇不同的實現方式。這包括將Java虛擬機代碼翻譯成另一種虛擬機的指令集或將其翻譯成宿主機處理程序的本地指令集。這種伸縮性使得虛擬機可以在性能、內存消耗和可移植性等方面進行權衡和優化。
- 即時編譯器(Just-In-Time Compiler)等例外情況: 在某些情況下,一些工具如調試器、性能監視器和即時編譯器可能需要訪問一些通常被認為是虛擬機后臺的元素,這可能對實現者的*度產生一些限制。
虛擬機實現者有很大的靈活性來調整實現以提高性能、降低內存消耗或實現其他目標,同時保持對Java虛擬機規范的兼容性。這種設計理念為不同的Java虛擬機實現提供了空間,以滿足各種不同的需求。
四、Class文件結構的發展
Class文件結構在Java技術體系中具有穩定性和可擴展性。以下是一些重要的觀點:
- Class文件結構的穩定性: 自《Java虛擬機規范》初版訂立以來,Class文件結構已經有二十多年的歷史。在這段時間里,盡管Java技術體系發生了巨大的改變,包括語言、API等方面的變化,但是Class文件結構一直保持相對穩定,主體結構和字節碼指令的語義和數量幾乎沒有變動。
- 對訪問標志和屬性表的改進: 隨著Java技術的演進,Class文件的訪問標志和屬性表也進行了一些改進。訪問標志新增了一些標志,如ACC_SYNTHETIC、ACC_ANNOTATION、ACC_ENUM、ACC_BRIDGE、ACC_VARARGS。屬性表集合中新增了一系列屬性,主要用于支持新的語言特性,如枚舉、變長參數、泛型、動態注解等,以及為了性能改進和調試信息。
- 平臺中立和可擴展性的重要性: Class文件格式具有平臺中立、緊湊、穩定和可擴展的特點,這是實現Java技術體系中平臺無關和語言無關兩項特性的關鍵支柱。這種設計使得Java程序可以在不同的硬件和操作系統上運行,同時為未來的語言特性和擴展提供了空間。
二十余年間,字節碼的數量和語義只發生過屈指可數的幾次變動,例如JDK1.0.2時改動過invokespecial指令的語義,JDK 7增加了invokedynamic指令,禁止了ret和jsr指令。
總結
以上是生活随笔為你收集整理的JVM学习-Class文件结构的全部內容,希望文章能夠幫你解決所遇到的問題。
 
                            
                        - 上一篇: 文心一言 VS 讯飞星火 VS chat
- 下一篇: 简易机器学习笔记(十一)opencv 简
