相關例項均使用Oracle JDK 1.8編譯,並使用javap生成位元組碼指令清單。
算術運算
Java虛擬機器通常基於運算元棧進行算術運算。只有iinc指令例外,它直接對區域性變數進行自增操作。
例項程式碼
int align2agrain(int i, int grain) {
return ((i + grain - 1) & ~(grain - 1));
}
複製程式碼
位元組碼指令序列
以上指令,並沒有出現取反的指令操作。因為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;
}
複製程式碼
位元組碼指令序列
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++;
}
}
複製程式碼
位元組碼指令序列
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++;
}
}
複製程式碼
位元組碼指令序列
由於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;
}
}
複製程式碼
位元組碼指令序列
if例項2
int greaterThan100(double d) {
if (d > 100.0) {
return 1;
} else {
return -1;
}
}
複製程式碼
位元組碼指令序列
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的運算有如下三種:
參考
《Java虛擬機器規範》(Java SE 8版)
個人公眾號
更多文章,請關注公眾號:二進位制之路