類文件結構
ClassFile {u4 magic;u2 minor_version;u2 major_version;u2 constant_pool_count;cp_info constant_pool[constant_pool_count-1];u2 access_flags;u2 this_class;u2 super_class;u2 interfaces_count;u2 interfaces[interfaces_count];u2 fields_count;field_info fields[fields_count];u2 methods_count;method_info methods[methods_count];u2 attributes_count;attribute_info attributes[attributes_count];
}
1. 魔數(magic)和版本號
- magic (4字節): 固定值0xCAFEBABE,用于識別類文件格式
- minor_version (2字節): 次版本號
- major_version (2字節): 主版本號,對應Java版本
- Java 8: 52 (0x34)
- Java 11: 55 (0x37)
- Java 17: 61 (0x3D)
2. 常量池(constant_pool)
- constant_pool_count (2字節): 常量池中項的數量加1(從1開始索引)
- constant_pool[]: 常量池表,包含多種類型的常量:
- Class信息 (CONSTANT_Class_info)
- 字段和方法引用 (CONSTANT_Fieldref_info, CONSTANT_Methodref_info)
- 字符串常量 (CONSTANT_String_info)
- 數值常量 (CONSTANT_Integer_info, CONSTANT_Float_info等)
- 名稱和描述符 (CONSTANT_NameAndType_info)
- 方法句柄和類型 (CONSTANT_MethodHandle_info, CONSTANT_MethodType_info)
- 動態調用點 (CONSTANT_InvokeDynamic_info)
3. 類訪問標志(access_flags)
表示類或接口的訪問權限和屬性,如:
- ACC_PUBLIC (0x0001): public類
- ACC_FINAL (0x0010): final類
- ACC_SUPER (0x0020): 使用新的invokespecial語義
- ACC_INTERFACE (0x0200): 接口
- ACC_ABSTRACT (0x0400): 抽象類
- ACC_SYNTHETIC (0x1000): 編譯器生成的類
- ACC_ANNOTATION (0x2000): 注解類型
- ACC_ENUM (0x4000): 枚舉類型
4. 類和父類信息
- this_class (2字節): 指向常量池中該類名的索引
- super_class (2字節): 指向常量池中父類名的索引(接口的super_class是Object)
5. 接口(interfaces)
- interfaces_count (2字節): 實現的接口數量
- interfaces[]: 每個元素是指向常量池中接口名的索引
6. 字段(fields)
- fields_count (2字節): 字段數量
- field_info[]: 字段表,每個字段包括:
- 訪問標志(如public, private, static等)
- 名稱索引(指向常量池)
- 描述符索引(指向常量池)
- 屬性表(如ConstantValue, Synthetic等)
7. 方法(methods)
- methods_count (2字節): 方法數量
- method_info[]: 方法表,每個方法包括:
- 訪問標志(如public, synchronized等)
- 名稱索引(指向常量池)
- 描述符索引(指向常量池)
- 屬性表(最重要的Code屬性包含字節碼)
8. 屬性(attributes)
- attributes_count (2字節): 屬性數量
- attribute_info[]: 屬性表,可能包含:
- SourceFile: 源文件名
- InnerClasses: 內部類列表
- EnclosingMethod: 用于局部類或匿名類
- Synthetic: 表示由編譯器生成
- Signature: 泛型簽名信息
- RuntimeVisibleAnnotations: 運行時可見注解
- BootstrapMethods: 用于invokedynamic指令
字節碼執行流程
有一段java代碼如下:
public class Demo02 {public static void main(String[] args) {int a = 10;int b = a++ + ++a + a--;}
}
- 常量池載入
運行時常量池
中 - 方法字節碼載入
方法區
中 - main線程開始運行,分配棧幀內存
1. 變量初始化 a = 10
0: bipush 10 // 將常量10壓入操作數棧
2: istore_1 // 將棧頂值(10)存儲到局部變量1(a)
此時內存狀態:
- 局部變量表:a = 10
- 操作數棧:[空]
2. 計算 a++
(第一個操作數)
3: iload_1 // 加載局部變量1(a)的值到棧頂 → 棧:[10]
4: iinc 1, 1 // 局部變量1(a)自增1 (a=11),注意這不會影響棧頂值
此時內存狀態:
- 局部變量表:a = 11
- 操作數棧:[10] (a++表達式的值是自增前的值)
3. 計算 ++a
(第二個操作數)
7: iinc 1, 1 // 局部變量1(a)先自增1 (a=12)
10: iload_1 // 然后加載a的值到棧頂 → 棧:[10, 12]
此時內存狀態:
- 局部變量表:a = 12
- 操作數棧:[10, 12] (++a表達式的值是自增后的值)
4. 第一次加法 a++ + ++a
11: iadd // 彈出棧頂兩個值相加,結果壓棧 → 10+12=22 → 棧:[22]
5. 計算 a--
(第三個操作數)
12: iload_1 // 加載a的值到棧頂 → 棧:[22, 12]
13: iinc 1, -1 // 局部變量1(a)自減1 (a=11),不影響棧頂值
此時內存狀態:
- 局部變量表:a = 11
- 操作數棧:[22, 12] (a–表達式的值是自減前的值)
6. 第二次加法 (前兩個之和) + a--
16: iadd // 22+12=34 → 棧:[34]
7. 存儲結果到b
17: istore_2 // 將棧頂值(34)存儲到局部變量2(b)
最終內存狀態:
- 局部變量表:a = 11, b = 34
- 操作數棧:[空]
完整字節碼序列
0: bipush 10 // a = 10
2: istore_1
3: iload_1 // 開始計算a++
4: iinc 1, 1
7: iinc 1, 1 // 開始計算++a
10: iload_1
11: iadd // 前兩個相加
12: iload_1 // 開始計算a--
13: iinc 1, -1
16: iadd // 與第三個相加
17: istore_2 // b = 結果
后置自增/減(i++):
- 先使用變量的當前值參與運算
- 執行自增/減操作
- 字節碼表現為先iload后iinc
前置自增/減(++i):
- 先執行自增/減操作
- 使用新值參與運算
- 字節碼表現為先iinc后iload
案例分析
分析i++
public class Demo02 {public static void main(String[] args) {int i = 0, x = 0;while(i < 10) {x = x++;++i;}System.out.println(x); // 0}
}
由于是x++,所以x先iload進入操作數棧【0】,再執行iinc進行自增【1】。自增后進行復制,又將操作數棧中的x賦值給x【0】,此時操作數棧中x的值為0。一次循環后,x的值還是0;最終x輸出0。
構造方法<cinit>()V
public class Demo03 {static int i = 10;static {i = 20;}static {i = 30;}public static void main(String[] args) {System.out.println(i); // 30}
}
編譯器會按照從上至下的順序, 收集所有的static靜態代碼塊和靜態成員賦值的代碼,合并成一個特殊的方法<cinit>()V
。
靜態變量和靜態代碼塊按代碼中的書寫順序依次執行,后執行的會覆蓋前邊的賦值。
構造方法<init>()V
public class Demo04 {private String a = "s1";{b = 20;}private int b = 10;{a = "s2";}public Demo04(String a, int b) {this.a = a;this.b = b;}public static void main(String[] args) {Demo04 d = new Demo04("s3", 30);System.out.println(d.a + " " + d.b); // s3 30}
}
編譯器會按照
從上往下的順序
,收集所有的{}代碼塊和成員變量賦值的代碼,形成新的構造方法,但是原始構造方法內的代碼總是在最后邊。
方法調用
public class Demo05 {private void test1(){}private final void test2(){}public void test3(){}public static void test4(){}public static void main(String[] args) {Demo05 d = new Demo05();d.test1();d.test2();d.test3();d.test4();Demo05.test4();}
}
每次調用方法的時候都是先把對象入棧,調用方法后再出棧。
對于使用對象調用靜態方法時(紫色框),先入棧再出棧,再調用,這樣相當于多了兩個無效的操作。所以如果要調用靜態方法時,推薦使用類調用。
多態的原理
public class Demo06 {public static void test(Animal animal) {animal.eat();System.out.println(animal);}public static void main(String[] args) throws IOException {test(new Cat());test(new Dog());System.in.read();}
}abstract class Animal {public abstract void eat();@Overridepublic String toString() {return "我是" + this.getClass().getSimpleName();}
}
class Dog extends Animal {public void eat() {System.out.println("啃骨頭");}
}class Cat extends Animal {public void eat() {System.out.println("吃魚");}
}
運行時的內存狀態:
test(new Cat())
調用時:- 堆中創建
Cat
對象 - 方法區中
Cat
類的虛方法表(vtable)包含:eat()
->Cat.eat()
toString()
->Animal.toString()
- 堆中創建
- 方法調用過程:
- JVM通過對象頭中的類指針找到
Cat
類 - 通過虛方法表找到實際要調用的
eat()
實現 toString()
調用則直接使用Animal
中的實現
- JVM通過對象頭中的類指針找到
finally案例1
public class Demo07 {public static void main(String[] args) {int i = 0;try {i = 10;}catch (Exception e) {i = 20;}finally {i = 30;}}
}
finally中的代碼會被復制三份,分別放入:try分支、catch能被匹配到的分支、catch不能被匹配到的分支,確保他一定被執行。
JVM使用異常表(Exception table)來確定異常處理跳轉位置,每個條目定義了受保護的代碼范圍(from-to)、處理代碼位置(target)和異常類型
finally案例2
public class Demo07 {public static int test() {try{int i = 1/0;return 10;}finally {return 20;}}public static void main(String[] args) {System.out.println(test()); // 20}
}
字節碼如下:
public static int test();Code:0: iconst_1 // 將1壓入棧1: iconst_0 // 將0壓入棧2: idiv // 執行除法(1/0),這里會拋出ArithmeticException3: istore_0 // (不會執行)存儲結果到局部變量04: bipush 10 // (不會執行)將10壓入棧6: istore_1 // (不會執行)存儲到局部變量1(臨時返回值)7: bipush 20 // finally塊開始:將20壓入棧9: ireturn // 直接從finally塊返回20// 異常處理部分10: astore_2 // 異常對象存儲到局部變量211: bipush 20 // finally塊:將20壓入棧13: ireturn // 從finally塊返回20Exception table:from to target type0 7 10 any
finally塊中的return會完全覆蓋try塊中的return或拋出的異常,這題輸出20而不會拋異常。(原本的ArithmeticException被丟棄,因為finally中有return)
控制流變化:
- 正常情況下:try → finally(return)
- 異常情況下:try → catch → finally(return)
- 兩種路徑最終都執行finally中的return
fianlly 案例3
public class Demo08 {public static int test() {int i = 10;try{return i;}finally {i = 20;}}public static void main(String[] args) {System.out.println(test()); // 10}
}
如果在try中return值了,就算在finally中修改了這個值,返回的結果也仍然不會改變,因為在return之前會先做一個暫存(固定返回值),然后執行finally中的代碼,再把暫存的值恢復到棧頂, 返回的還是之前暫存的值。