JVM指令分析實例四(數組、switch)

本篇爲《JVM指令分析實例》的第四篇,相關實例均使用Oracle JDK 1.8編譯,並使用javap生成字節碼指令清單。java

前幾篇傳送門:數組

JVM指令分析實例一(常量、局部變量、for循環)bash

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

JVM指令分析實例三(方法調用、類實例)spa

數組

一維原始類型數組

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指令code

建立一個指定原始類型(如int、float、char等)的數組,並將其引用值壓入棧頂。對象

執行該指令後,將從操做數棧出棧1個參數count,類型爲int,表示要建立數組的大小。排序

iastore指令索引

從操做數棧讀取一個int類型數據並存入指定數組中。接口

執行該指令後,將從操做數棧出棧3個參數arrayref、index和value,在本例中分別對應於第十、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,在本例中分別對應於第八、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版)

相關文章
相關標籤/搜索