JVM指令分析例項四(陣列、switch)

二進位制之路發表於2019-02-24

本篇為《JVM指令分析例項》的第四篇,相關例項均使用Oracle JDK 1.8編譯,並使用javap生成位元組碼指令清單。

前幾篇傳送門:

JVM指令分析例項一(常量、區域性變數、for迴圈)

JVM指令分析例項二(算術運算、常量池、控制結構)

JVM指令分析例項三(方法呼叫、類例項)

陣列

一維原始型別陣列

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″。


題圖:codeforwin.org

參考

《The Java Virtual Machine Specification, Java SE 8 Edition》

《Java虛擬機器規範》(Java SE 8版)

相關文章