JVM指令分析例項三(方法呼叫、類例項)
本篇為《JVM指令分析例項》的第三篇,相關例項均使用Oracle JDK 1.8編譯,並使用javap生成位元組碼指令清單。
前兩篇傳送門:
ofollow,noindex">JVM指令分析例項一(常量、區域性變數、for迴圈)
方法引數
方法的區域性變量表,索引值從0開始,且小於區域性變量表的長度。
對於例項方法,JVM會隱式傳遞一個指向當前例項的引用(this),作為方法的第0個區域性變數。因此,應用程式實際傳遞的引數是從索引值1開始的。
但是,對於類方法,由於不需要傳遞例項引用。因此,應用程式實際傳遞的引數是從索引值0開始的。
例項程式碼
int addTwo(int i, int j) { return i + j; } static int addTwoStatic(int i, int j) { return i + j; } 複製程式碼
位元組碼指令序列

方法呼叫
invokevirtual
該指令用於 呼叫物件的例項方法 ,根據物件的實際型別進行分派(虛方法分派)。
指令帶有一個表示索引的引數,執行時常量池在該索引處的項為某個方法的符號引用(提供類名稱、方法名稱及方法描述符資訊)。
invokestatic
該指令用於 呼叫類方法(static方法) 。
invokespecial
該指令用於呼叫一些需要特殊處理的例項方法,包括 例項初始化方法、私有方法和父類方法 。
呼叫例項方法程式碼
int add12and13() { return addTwo(12, 13); } 複製程式碼
位元組碼指令序列

具體執行流程如下:

常量池
invokevirtual的引數為常量池索引值16,對應於常量池的Methodref常量型別,表示一個方法。Methodref結構主要包括兩部分,Class(類或介面)和NameAndType(欄位或方法),最終都指向字串常量型別Utf8。
方法的符號引用最終解析結果為:jvm/specification/se8/chapter3/MethodInvoke.addTwo:(II)I,格式為:類全限定名.方法名:方法描述符。

呼叫類方法程式碼
int add12and13() { return addTwoStatic(12, 13); } 複製程式碼
位元組碼指令序列

呼叫類方法與呼叫例項方法相比,指令序列主要有兩點差異:
- 呼叫類方法不需要傳入例項引用this,因此不需要壓入棧。
- 呼叫類方法使用指令invokestatic(而不是invokevirtual)。
invokespecial例項程式碼
class Near { int it; public int getItNear() { return getIt(); // 呼叫私有方法 } private int getIt() { return it; } } class Far extends Near{ int getItFar() { return super.getItNear(); // 呼叫父類方法 } } 複製程式碼
位元組碼指令序列
jvm.specification.se8.chapter3.Near():// 呼叫Near父類Object的例項初始化方法 0: aload_0 1: invokespecial #10// Method java/lang/Object."<init>":()V 4: return public int getItNear():// 呼叫getIt()私有方法 0: aload_0 1: invokespecial #18// Method getIt:()I 4: ireturn int getItFar():// 呼叫父類getItNear()方法 0: aload_0 1: invokespecial #16// Method jvm/specification/se8/chapter3/Near.getItNear:()I 4: ireturn 複製程式碼
invokespecial指令用於呼叫例項初始化方法、私有方法和父類方法。
與普通例項方法類似,使用invokespecial指令呼叫的方法都需要以this作為首個引數。
使用類例項
例項程式碼1
Object create() { return new Object(); } 複製程式碼
位元組碼指令序列
java.lang.Object create(): 0: new#3 // class java/lang/Object. 建立物件,並將其引用值壓入棧頂 3: dup// 複製棧頂引用值,並將複製值壓入棧頂 4: invokespecial #8 // Method java/lang/Object."<init>":()V. 呼叫例項初始化方法 7: areturn// 從當前方法返回物件引用 複製程式碼
new指令用於建立一個物件。dup指令用於複製棧頂數值,並將複製值壓入棧頂。
之所以需要在建立物件之後,再複製一個引用,是因為invokespecial和areturn各需要1個物件引用值。
注意,new指令執行後,並沒有完成一個物件例項建立的全部過程,只有執行和完成了例項初始化方法後,例項才算建立完全。
例項程式碼2
public class MyObj { int i; MyObj example() { MyObj o = new MyObj(); return silly(o); } MyObj silly(MyObj o) { if (o != null) { return o; } else { return o; } } } 複製程式碼
位元組碼指令序列
jvm.specification.se8.chapter3.MyObj example(): 0: new#1 // class jvm/specification/se8/chapter3/MyObj. 建立MyObj物件並將引用壓入棧頂 3: dup// 複製棧頂引用值,並將複製值壓入棧頂 4: invokespecial #18 // Method "<init>":()V. 呼叫MyObj例項初始化方法 7: astore_1// 將棧頂引用值存入第2個區域性變數(o) 8: aload_0// 將第1個區域性變數壓入棧頂(this) 9: aload_1// 將第2個區域性變數壓入棧頂(o) 10: invokevirtual #19 // Method silly:(Ljvm/specification/se8/chapter3/MyObj;)Ljvm/specification/se8/chapter3/MyObj; 呼叫例項方法(silly),並將返回結果壓入棧頂 13: areturn// 從當前方法返回物件引用 jvm.specification.se8.chapter3.MyObj silly(jvm.specification.se8.chapter3.MyOb j): 0: aload_1// 將第2個區域性變數壓入棧頂(o) 1: ifnull6// 如果棧頂數值為null,則跳轉到索引號為6的指令繼續執行 4: aload_1// 將第2個區域性變數壓入棧頂(o) 5: areturn// 從當前方法返回物件引用 6: aload_1// 將第2個區域性變數壓入棧頂(o) 7: areturn// 從當前方法返回物件引用 複製程式碼
例項程式碼3
public class InstanceFieldGetSet { int i; void setIt(int value) { i = value; } int getIt() { return i; } } 複製程式碼
位元組碼指令序列
void setIt(int): 0: aload_0// 將第1個區域性變數this壓入棧頂 1: iload_1// 將第2個區域性變數value壓入棧頂 2: putfield#18 // Field i:I. 設定this例項的i欄位值為value 5: return int getIt(): 0: aload_0// 將第1個區域性變數this壓入棧頂 1: getfield#18 // Field i:I. 獲取this例項的欄位i的值,並壓入棧頂 4: ireturn// 從當前方法返回int型別結果 複製程式碼
類例項的欄位使用getfield和putfield指令進行訪問。
與方法呼叫指令的運算元類似,putfield及getfield指令的運算元也不代表該欄位在類例項中的偏移量。編譯器會為例項的這些欄位生成符號引用,並儲存在執行時常量池之中。這些執行時常量池項會在執行階段解析為受引用物件中的真實欄位位置。