本篇為《JVM指令分析例項》的第四篇,相關例項均使用Oracle JDK 1.8編譯,並使用javap生成位元組碼指令清單。
前幾篇傳送門:
陣列
一維原始型別陣列
void createBuffer() {
int buffer[];
int bufsz = 100;
int value = 12;
buffer = new int[bufsz];
buffer[10] = value;
value = buffer[11];
}
複製程式碼
位元組碼指令序列
void createBuffer():
0: bipush 100 // 將單位元組int常量值100壓入棧頂
2: istore_2 // 將棧頂int型別數值100存入第3個區域性變數. bufsz = 100
3: bipush 12 // 將單位元組int常量值12壓入棧頂
5: istore_3 // 將棧頂int型別數值12存入第4個區域性變數. value = 12
6: iload_2 // 將第3個int型別區域性變數壓入棧頂
7: newarray int // 建立int型別陣列,並將陣列引用值壓入棧頂. new int[bufsz]
9: astore_1 // 將棧頂引用型別值存入第2個區域性變數. buffer = new int[bufsz]
10: aload_1 // 將第2個引用型別區域性變數壓入棧頂
11: bipush 10 // 將單位元組int常量10壓入棧頂
13: iload_3 // 將第4個int型別區域性變數壓入棧頂
14: iastore // 將棧頂int型別數值存入陣列的指定索引位置. buffer[10] = value
15: aload_1 // 將第2個引用型別值壓入棧頂
16: bipush 11 // 將單位元組int常量值11壓入棧頂
18: iaload // 將int型別陣列的指定元素壓入棧頂
19: istore_3 // 將棧頂int型別數值存入第4個區域性變數
20: return
複製程式碼
newarray指令
建立一個指定原始型別(如int、float、char等)的陣列,並將其引用值壓入棧頂。
執行該指令後,將從運算元棧出棧1個引數count,型別為int,表示要建立陣列的大小。
iastore指令
從運算元棧讀取一個int型別資料並存入指定陣列中。
執行該指令後,將從運算元棧出棧3個引數arrayref、index和value,在本例中分別對應於第10、11和13索引位置壓入的值。
其中,arrayref是一個引用型別值,指向一個int型別的陣列。index和value為int型別,index表示待存入陣列位置的索引號,value表示待存入index索引位置的值。
iaload指令
從陣列中載入一個int型別資料到運算元棧。
執行該指令後,將從運算元棧出棧2個引數arrayref和index,在本例中分別對應於第15和16索引位置壓入的值。
其中,arrayref是一個引用型別值,指向一個int型別的陣列。index為int型別,表示待載入陣列資料的索引號。
一維引用型別陣列
void createThreadArray() {
Thread threads[];
int count = 10;
threads = new Thread[count];
threads[0] = new Thread();
}
複製程式碼
位元組碼指令序列
void createThreadArray():
0: bipush 10 // 將單位元組int型別值10壓入棧頂
2: istore_2 // 將棧頂int型別值存入第3個區域性變數. count = 10
3: iload_2 // 將第3個int型別區域性變數壓入棧頂
4: anewarray #15 // class java/lang/Thread. 建立Thread型別陣列,並將陣列引用值壓入棧頂. new Thread[count]
7: astore_1 // 將棧頂引用型別值存入第2個區域性變數
8: aload_1 // 將第2個引用型別區域性變數壓入棧頂
9: iconst_0 // 將int型別常量0壓入棧頂
10: new #15 // class java/lang/Thread. 建立Thread物件,並將引用值壓入棧頂
13: dup // 複製棧頂值並壓入棧頂
14: invokespecial #17 // Method java/lang/Thread."<init>":()V. 呼叫例項初始化方法
17: aastore // 將棧頂引用型別值存入陣列的指定索引位置. threads[0] = new Thread()
18: return
複製程式碼
anewarray指令
建立一個引用型別(如類、介面、陣列)陣列,並將其引用值壓入棧頂。可用於建立一維引用陣列,或者用於建立多維陣列的一部分。
執行該指令後,將從運算元棧出棧1個引數count,型別為int,表示要建立陣列的大小。
aastore指令
(aastore指令與iastore指令作用類似)
從運算元棧讀取一個引用型別資料並存入指定陣列中。
執行該指令後,將從運算元棧出棧3個引數arrayref、index和value,在本例中分別對應於第8、9和10索引位置壓入的值。
其中,arrayref是一個引用型別值,指向一個引用型別的陣列。index為int型別,index表示待存入陣列位置的索引號。value為引用型別,表示待存入index索引位置的值。
在執行時,value的實際型別必須與arrayref所代表的陣列的元件型別相匹配。
多維陣列
int[][][] create3DArray() {
int grid[][][];
grid = new int[10][5][];
return grid;
}
複製程式碼
位元組碼指令序列
int[][][] create3DArray():
0: bipush 10 // 將單位元組int型別值10壓入棧頂. 第1維
2: iconst_5 // 將int型別常量5壓入棧頂. 第2維
3: multianewarray #16, 2 // class "[[[I". 建立int[][][]型別陣列,並將引用值壓入棧頂
7: astore_1 // 將棧頂引用型別值存入第2個區域性變數
8: aload_1 // 將第2個引用型別區域性變數壓入棧頂
9: areturn // 從當前方法返回棧頂引用型別值
複製程式碼
multianewarray指令
建立指定型別和指定維度的多維陣列(執行該指令時,運算元棧中必須包含各維度的長度值),並將其引用值壓入棧頂。可以用於建立所有型別的多維陣列。
對於本例項,陣列型別為[[[I,即#16對應的常量池中的符號引用。陣列維度為2,兩個維度的長度值分別為10和5。雖然int[][][]為3維陣列,但由於僅指定了前2個維度的長度值,因此指令對應的維度值為2。
如果指定了第3個維度的長度值,那麼在iconst_5之後還需要再將1個int型別長度值壓入棧。
所有的陣列都有一個與之關聯的長度屬性,可通過arraylength指令訪問。
switch語句
編譯器會使用tableswitch和lookupswitch指令來生成switch語句的編譯程式碼。
Java虛擬機器的tableswitch和lookupswitch指令都只能支援int型別的條件值。
tableswitch指令可以高效地從索引表中確定case語句塊的分支偏移量。
當switch語句中的case分支條件值比較稀疏時,tableswitch指令的空間使用率偏低。這種情況下,可以使用lookupswitch指令來代替。
tableswitch指令
int chooseNear(int i) {
switch(i) {
case 0: return 0;
case 1: return 1;
case 2: return 2;
default: return -1;
}
}
複製程式碼
位元組碼指令序列
int chooseNear(int):
0: iload_1 // 將第2個int型別區域性變數壓入棧頂
1: tableswitch { // 0 to 2
0: 28 // 如果case條件值為0,則跳轉到索引號為28的指令繼續執行
1: 30 // 如果case條件值為1,則跳轉到索引號為30的指令繼續執行
2: 32 // 如果case條件值為2,則跳轉到索引號為32的指令繼續執行
default: 34 // 否則,則跳轉到索引號為34的指令繼續執行
}
28: iconst_0 // 將int型別常量0壓入棧頂
29: ireturn // 從當前方法返回棧頂int型別數值
30: iconst_1 // 將int型別常量1壓入棧頂
31: ireturn // 從當前方法返回棧頂int型別數值
32: iconst_2 // 將int型別常量2壓入棧頂
33: ireturn // 從當前方法返回棧頂int型別數值
34: iconst_m1 // 將int型別常量-1壓入棧頂
35: ireturn // 從當前方法返回棧頂int型別數值
複製程式碼
tableswitch指令
用於switch條件跳轉,case值連續(變長指令)。
根據索引值在跳轉表中尋找配對的分支並進行跳轉。
指令格式:tableswitch padbytes defaultbytes lowbytes highbytes jumptablebytes
- padbytes:0~3個填充位元組,以使得defaultbytes與方法起始地址(方法內第一條指令的操作碼所在的地址)之間的距離是4的位數。
- defaultbytes:32位預設跳轉地址
- lowbytes:32位低值low
- highbytes:32位高值high
- jumptablebytes:(high-low+1)個32位有符號數值形成的一張零基址跳轉表(0-based jump table)
由於採用了索引值定位的方式(可理解為陣列隨機訪問),因此只需要檢查索引是否越界,非常高效。
下面結合例項分析一下:
第1條指令的索引號為0,tableswitch指令索引號為1,為了使defaultbytes與方法起始地址之間的距離是4的位數,所以defaultbytes的開始索引號為4。
defaultbytes、lowbytes和highbytes分別佔4個位元組,總共12個位元組。
case高低值分別為2和0,因此jumptablebytes佔用(2-0+1)*4=12個位元組。
由於defaultbytes的開始索引號為4,defaultbytes~jumptablebytes共佔用24個位元組,因此緊跟在tableswitch後面的下一條指令的索引號為4+24=28,對應於例項中的指令”28: iconst_0″。
這裡順便提一下,一般情況下,普通的運算元佔1個位元組,指向常量池的索引值佔2個位元組(ldc的常量池索引佔1個位元組,ldc_w、ldc2_w的常量池索引佔2個位元組)。所以,方法的指令索引號之間有時不是連續的。
lookupswitch指令
int chooseFar(int i) {
switch(i) {
case -100: return -1;
case 0: return 0;
case 100: return 1;
default: return -1;
}
}
複製程式碼
位元組碼指令序列
int chooseFar(int):
0: iload_1
1: lookupswitch { // 3
-100: 36
0: 38
100: 40
default: 42
}
36: iconst_m1
37: ireturn
38: iconst_0
39: ireturn
40: iconst_1
41: ireturn
42: iconst_m1
43: ireturn
複製程式碼
lookupswitch指令
用於switch條件跳轉,case值不連續(變長指令)。
根據鍵值(非索引)在跳轉表中尋找配對的分支並進行跳轉。
指令格式:lookupswitch padbytes defaultbytes npairsbytes matchoffsetbytes
- padbytes:0~3個填充位元組,以使得defaultbytes與方法起始地址(方法內第一條指令的操作碼所在的地址)之間的距離是4的位數。
- defaultbytes:32位預設跳轉地址
- npairsbytes:32位匹配鍵值對的數量npairs
- matchoffsetbytes:npairs個鍵值對,每一組鍵值對都包含了一個int型別值match以及一個有符號32位偏移量offset。
由於case條件值是非連續的,因此無法採用像tableswitch直接定位的方式,必須對每個鍵值進行比較。然而,JVM規定,lookupswitch的跳轉表必須根據鍵值排序,這樣(如採用二分查詢)會比線性掃描更有效率。
下面結合例項分析一下:
第1條指令的索引號為0,lookupswitch指令索引號為1,為了使defaultbytes與方法起始地址之間的距離是4的位數,所以defaultbytes的開始索引號為4。
defaultbytes、npairsbytes分別佔4個位元組,總共8個位元組。
case有3個條件,共3個鍵值對(npairs為3)。由於每個鍵值對佔8個位元組(4位元組match+4位元組offset),因此matchoffsetbytes共佔24個位元組。
所以,緊跟在lookupswitch後面的下一條指令的索引號為4+8+24=36,對應於例項中的指令”36: iconst_m1″。
參考
《The Java Virtual Machine Specification, Java SE 8 Edition》
《Java虛擬機器規範》(Java SE 8版)