java 文件經過javac編譯后,變成了存儲了一系列指令的.class文件。本文從指令層面分析Java 方法從解析、調用到執行的過程。
1 指令
一般格式:操作碼 [操作數1] [操作數2] ...
操作碼 | 1個字節的無符號整數(范圍:0x00 ~ 0xFF)。 特點:1)每個操作碼對應一個助記符(如iconst_1,iload,iadd等)。2)操作碼決定了操作數類型及個數。 |
操作數 | 操作碼所需的參數,緊跟在操作碼后面。 |
表 指令的組成
1.1 未對齊的操作數
操作數的基本單位是字節。為了讓.class文件更緊湊,jvm沒有讓操作數對齊。這意味著JVM需要逐個字節讀取。
例如指令:0x11 0x03 0xE8 (sipush 1000)
0x11 是操作碼,它的助記符是sipush。這個操作碼的參數是1個2字節長度的操作數。
jvm 先讀取第1個字節0x03,然后讀取第2個字節 0xE8,最后將這兩個字節組合:
(0x03 << 8) | 0xE8 => 0x03E8。
1.2 指令的執行模型
不考慮異常處理的話,JVM的解釋器解析.class文件中的指令偽代碼如下:
do {PC 寄存器值++;根據PC寄存器指示的位置,讀取操作碼;if (操作碼需要操作數) 讀取操作數;執行操作碼所定義的操作;
} while(字節流長度 > 0);
1.3 方法相關指令
invokestatic | 靜態方法。 |
invokespecial | 需要特殊處理的實例方法,包括實例初始化、私有方法、和super調用。 |
invokevirtual | 虛方法分派(可被重寫的方法及final方法)。 |
invokeinterface | 接口方法,在運行期間再確定一個實現該接口的對象。 |
invokedynamic | 先在運行時動態解析調用點限定符所引用的方法,然后再執行該方法。Java 7 引入,用于支持動態語言特性,比如Lambda |
表 調用方法的指令
方法調用指令和數據類型無關,而方法返回指令根據返回值類型區分,包括ireturn、lreturn、freturn、dreturn、areturn(返回值為對象、數組等引用類型)及return(返回值為void)。
1.3.1?類與實例的初始化
<clinit> | 類(或接口)的靜態初始化方法。用于執行靜態變量的賦值和靜態代碼塊。 如果父類未初始化,會先觸發父類的clinit方法,但接口的clinit不會因為實現類的初始化而觸發(需要直接使用接口的靜態變量才觸發)。 |
<init> | 對象的初始化方法,用于實例變量的賦值、實例代碼塊及構造器。 每個構造器對應一個init方法;子類構造器會隱式調用父類的init方法。 |
表 類與實例初始化方法
<clinit> 與<init>方法都是由編譯器自動生成,用戶無法調用。
2 方法調用
java 是一門靜態多分派(和接收者及參數有關)、動態單分派(只能接收者有關)的語言。
靜態分派:方法的靜態類型在編譯階段是可知的(如方法重載,取決于參數類型、數量及位置)。
動態分派:運行時類型要在運行期才可知(方法重寫,取決于執行對象的實際類型)。
2.1 虛方法表
JVM 在類初始化過程中,會為這個類維護一個虛擬方法表,存儲該類所有可被重寫的方法的入口地址。
虛方法表可理解為一個數組,每個數組元素(槽位)存儲的是方法的入口地址。
虛方法表創建步驟如下:
1)父類的方法按聲明順序占據虛方法表的固定槽位。
2)子類繼承父類的虛方法表,并保留父類方法的地址。
3)如果子類重寫了父類的方法,子類的虛方法中對應的槽位會被替換為子類方法的地址。
4)子類新增的方法追加到虛方法表的末尾。
例如 Animal類又兩個可重寫的方法sound和eat,Dog類繼承Animal類,并重寫了eat方法,又新增了一個方法wagTail。則這兩個類的虛方法表如下。
Animal類的虛方法表 | Dog類的虛方法表 | ||
索引 | 方法地址 | 索引 | 方法地址 |
0 | Animal.sound() | 0 | Animal.sound() |
1 | Animal.eat() | 1 | Dog.eat() |
2 | Dog.wagTail() |
表 Animal 與 Dog類的虛方法表
2.1.1 動態分派過程
當父類引用調用方法時,JVM執行步驟如下:
- 獲取對象的實際類型。
- 根據對象的實際類型查找對應的虛方法表。
- 根據方法調用指令的操作數(虛方法表的索引),找到方法入口地址。
- 執行對應方法。