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

二進位制之路發表於2018-10-01

相關例項均使用Oracle JDK 1.8編譯,並使用javap生成位元組碼指令清單。

算術運算

Java虛擬機器通常基於運算元棧進行算術運算。只有iinc指令例外,它直接對區域性變數進行自增操作。

例項程式碼

int align2agrain(int i, int grain) {
	return ((i + grain - 1) & ~(grain - 1));
}
複製程式碼

位元組碼指令序列

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

以上指令,並沒有出現取反的指令操作。因為JVM並沒有提供取反指令,而是使用異或指令來實現取反。

對一個數進行取反,相當於該數的二進位制每一位與1進行異或操作。由於-1的補碼二進位制表示為全部都是1,因此對一個數進行取反,也相當於-1與該數進行異或。

-1的原碼、反碼和補碼錶示

[10000001]原=[11111110]反=[11111111]補
複製程式碼

異或實現取反

~x = -1^x
複製程式碼

訪問執行時常量池

例項程式碼

void useManyNumeric() {
	int i = 100;
	int j = 1000000;
	long l1 = 1;
	long l2 = 0xffffffff;
	double d = 2.2;
}
複製程式碼

位元組碼指令序列

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

ldc、ldc_w:將int、float或String型別常量值從常量池中推送至棧頂。

ldc2_w:將long、double型別常量值從常量池中推送至棧頂。(只有寬索引版本)

其中,ldc_w和ldc2_w屬於寬索引指令,即指令對應的(索引值)引數為2個位元組。而ldc指令對應的(索引值)引數為1個位元組。

當執行時常量池中的常量個數超過256個(1個位元組所能代表的數量)時,需要使用支援2個位元組索引值的指令ldc_w指令來代替ldc訪問常量池

在區域性變數表中,long和double型別的資料佔用兩個連續的區域性變數,並且採用兩個區域性變數中較小的索引值來定位其資料。 因此,lstore_3、lstore 5、dstore 7 這三個指令實際存入的區域性變數索引號分別為3和4、5和6、7和8。(區域性變數表的索引值從0開始)

控制結構

Java虛擬機器會根據資料型別的變化來生成不同的條件跳轉語句。

while例項1

void whileInt() {
	int i = 0;
	while (i < 100) {
		i++;
	}
}
複製程式碼

位元組碼指令序列

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

iinc用於實現區域性變數的自增操作。在所有位元組碼指令中,只有該指令可直接用於操作區域性變數。

對於迴圈的實現,將條件判斷放在迴圈的最前面不是更易於理解,為什麼要放在最後面?讓我們來看看放在最前面的指令序列:

2 iload_1
3 bipush 100
5 if_icmpge 14
8 iinc 1,1
11 goto 2
複製程式碼

顯然,兩種實現方式第1次迴圈都要執行5條執行。但對於後續的迴圈,前者只需要執行4條指令,而後者則需要執行5條指令。因此,將條件判斷放在迴圈的最後面可以更高效的執行迴圈

while例項2

void whileDouble() {
	double i = 0;
	while (i < 100.1) {
		i++;
	}
}
複製程式碼

位元組碼指令序列

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

由於iinc只針對int型別的區域性變數進行自增操作,JVM並沒有提供相應的指令來操作double型別。因此,需要藉助dadd來實現double型別的自增操作。

同樣,對於數值型別,以if開頭的比較跳轉指令,都只支援int型別(對於非數值型別,if比較跳轉指令還支援引用型別數值)。因此,JVM另外提供了dcmpg、dcmpl來比較兩個double型別數值的大小,然後將比較結果(1,0,-1)壓入棧頂。最後,再使用int型別的if判斷指令來進行判斷跳轉。

dcmpg與dcmpl的區別僅在於,當比較的其中一個值為NaN時,dcmpg將1壓入棧頂,而dcmpl將-1壓入棧頂。

ldc相關指令都是將常量值從常量池中推至棧頂,前面"訪問執行時常量池"一節已經介紹過了。

對於for迴圈分析,請看第一篇:JVM指令分析例項一(常量、區域性變數、for迴圈)

if例項1

int lessThan100(double d) {
	if (d < 100.0) {
		return 1;
	} else {
		return -1;
	}
}
複製程式碼

位元組碼指令序列

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

if例項2

int greaterThan100(double d) {
	if (d > 100.0) {
		return 1;
	} else {
		return -1;
	}
}
複製程式碼

位元組碼指令序列

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

if例項2與if例項1的差別僅在於比較符號由小於號改為大於號,因此ifge指令也相應的變成ifle指令。

如果細心一點,還會發現一個差異,double比較指令由dcmpg變成了dcmpl。

那麼,JVM在什麼情況下使用dcmpg,什麼情況下又會使用dcmpl呢?為了理解這一點,我們需要先回顧一下浮點數中的NaN值。

Java虛擬機器關於浮點數的規範

浮點型別包含float和double型別兩種,32位單精度和64位雙精度與IEEE 754格式的取值與操作是一致的。

NaN值用於表示某此無效的運算操作,例如0除以0等情況。

只要有運算元是NaN,那麼對它進行任何數值比較和等值測試都會返回false。任何數值與NaN進行不等值比較都會返回true。

有了以上知識,我們再回到例子來分析一下。

我們知道,dcmpg與dcmpl的作用都是比較兩個double型別數值的大小,並將結果(1,0,-1)壓入棧頂。區別僅在於,當比較的其中一個值為NaN時,dcmpg將1壓入棧頂,而dcmpl將-1壓入棧頂。

對於 if (d < 100.0) {},隱含了兩個條件,一個是d必須小於100.0,另一個是d不能為NaN(如果為NaN會返回false)。因此,NaN屬於該條件之外的情況。

當 if (d < 100.0) {} 成立時,執行比較指令之後結果為-1。由於滿足該條件時d不能為NaN,顯然當d為NaN時比較結果不能為-1。因此比較指令排除dcmpl,只能使用dcmpg指令。

維基百科對NaN的定義

NaN(Not a Number,非數)是電腦科學中數值資料型別的一類值,表示未定義或不可表示的值。常在浮點數運算中使用。首次引入NaN的是1985年的IEEE 754浮點數標準。

返回NaN的運算有如下三種:

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

參考

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

NaN:zh.wikipedia.org/wiki/NaN

個人公眾號

更多文章,請關注公眾號:二進位制之路

二進位制之路

相關文章