文章目錄
一、概述
JAVA 中的 boolean 類型是我們經常使用的一個類型,但是我們對其瞭解可能是僅限於 true 和 false,因此本篇博文將帶你從 Java 虛擬機字節碼的角度來認識不一樣的 boolean 類型。
二、實例代碼和指令
2.1 示例代碼和指令
// Fool.java
public class Foo {
public static void main(String[] args) {
boolean flag = true;
if (flag) System.out.println("Hello, Java!");
if (flag == true) System.out.println("Hello, JVM!");
}
}
$ echo '
public class Foo {
public static void main(String[] args) {
boolean flag = true;
if (flag) System.out.println("Hello, Java!");
if (flag == true) System.out.println("Hello, JVM!");
}
}' > Foo.java
$ javac Foo.java
$ java Foo
$ java -cp /path/to/asmtools.jar org.openjdk.asmtools.jdis.Main Foo.class > Foo.jasm.1
$ awk 'NR==1,/iconst_1/{sub(/iconst_1/, "iconst_2")} 1' Foo.jasm.1 > Foo.jasm
$ java -cp /path/to/asmtools.jar org.openjdk.asmtools.jasm.Main Foo.jasm
$ java Foo
2.2 運行結果
三、探究 boolean 類型
3.1 指令解析
$ echo '
public class Foo {
public static void main(String[] args) {
boolean flag = true;
if (flag) System.out.println("Hello, Java!");
if (flag == true) System.out.println("Hello, JVM!");
}
}' > Foo.java
$ javac Foo.java
$ java Foo
$ java -cp /path/to/asmtools.jar org.openjdk.asmtools.jdis.Main Foo.class > Foo.jasm.1
$ awk 'NR==1,/iconst_1/{sub(/iconst_1/, "iconst_2")} 1' Foo.jasm.1 > Foo.jasm
$ java -cp /path/to/asmtools.jar org.openjdk.asmtools.jasm.Main Foo.jasm
$ java Foo
- 將示例的 Java 代碼輸出至 Foo.java 文件中;
- 使用 javac 編譯 Foo.java ;
- 運行 Foo 類的 Main 方法;
- 使用 AsmTools 將 .class 字節碼文件轉換爲 JASM 語法並將轉換後的結果輸出至 Foo.jasm.1 文件中;
- 使用 Linux 的 awk 命令在 Foo.jasm.1 文件中查找字符串 “iconst_1” 將其替換爲 “iconst_2” 並將替換後的文件內容輸出至 Foo.jasm 文件中;
- 使用 AsmTools 將 JASM 語法文件 Foo.jasm 轉換爲 .class 字節碼文件;
- 運行 Foo 類的 Main 方法;
3.2 JASM 文件
public static Method main:"([Ljava/lang/String;)V"
stack 2 locals 2
{
iconst_1;
istore_1;
iload_1;
ifeq L14; "第一處判斷語句 if (flag)"
getstatic Field java/lang/System.out:"Ljava/io/PrintStream;";
ldc String "Hello, Java!";
invokevirtual Method java/io/PrintStream.println:"(Ljava/lang/String;)V";
L14: stack_frame_type append;
locals_map int;
iload_1;
iconst_1;
if_icmpne L27; "第二處判斷語句 if (flag == true)"
getstatic Field java/lang/System.out:"Ljava/io/PrintStream;";
ldc String "Hello, JVM!";
invokevirtual Method java/io/PrintStream.println:"(Ljava/lang/String;)V";
L27: stack_frame_type same;
return;
}
} // end Class Foo
3.3 .class 字節碼文件
public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=2, locals=2, args_size=1
0: iconst_1
1: istore_1
2: iload_1
3: ifeq 14 // 第一處判斷語句 if (flag)
6: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;
9: ldc #3 // String Hello, Java!
11: invokevirtual #4 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
14: iload_1
15: iconst_1
16: if_icmpne 27 // 第二處判斷語句 if (flag == true)
19: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;
22: ldc #5 // String Hello, JVM!
24: invokevirtual #4 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
27: return
LineNumberTable:
line 1: 0
StackMapTable: number_of_entries = 2
frame_type = 252 /* append */
offset_delta = 14
locals = [ int ]
frame_type = 12 /* same */
}
3.4 操作解析和原理分析
通過上面的 JASM 語法以及 .class 字節碼我們可以看到對於原代碼中的第一處判斷語句 if (flag) 以及後面第二處判斷語句 if (flag == true) 在字節碼中分別被翻譯爲 ifeq 和 if_icmpne 指令,而這兩條指令的含義如下:
- ifeq :當操作數棧上數值爲 0 時跳轉;
- if_icmpne :當操作數棧上兩個數值不相同時跳轉
因此首先我們可以得出的一條結論即對於 Java 中的 if (boolean) 語句在 Java 虛擬機上會被翻譯爲如果該 boolean 值爲零那麼就進行跳轉,也即暫時可以判斷在虛擬機中 boolean 是被當做整形來看待的,接下的 if (flag == true) 語句則比較兩個數值是否相等,即當兩個數值不相等時跳轉。
然後回到我們剛剛的示例指令中,對於上面的指令我們可以分爲兩部分來看:
- 首先我們將原源代碼直接編譯爲字節碼文件,通過字節碼文件我們可以看到此時 flag 的值爲 iconst_1(常數 1),所以在進行第一個判斷指令 ifeq 的判斷時因爲 flag 不爲零所以不進行跳轉,因此打印了 Hello, Java! ,之後再進行第二個 if_icmpne 判斷指令的判斷,因爲 flag 的值爲 iconst_1 等於 true(iconst_1),所以打印了第二個 Hello, JVM! ;
- 接下來我們將字節碼轉換爲 JASM 語言格式的文件,然後通過 Linux 的 awk 命令將文件中的 iconst_1(常數 1)替換爲了 iconst_2(常數 2),然後再通過 ASM 將其重新編譯爲字節碼文件。在這次的運行中對於第一個判斷指令 ifeq 的判斷因爲 flag 爲 iconst_2 即仍然不爲零,所以仍然不進行跳轉,依舊打印了 Hello, Java! ,但對於第二個 if_icmpne 的判斷因爲此時 flag 爲 iconst_2 不等於 true 的 iconst_1 ,所以並沒有輸出 Hello, JVM! ;
通過上面的測試我們可以得出這樣的結論:在 Java 虛擬機中 boolean 類型被映射成 int 類型,具體來說,true 被映射爲整數 1,而 false 被映射爲整數 0。對於 Java 中普通的 if (flag) 判斷實質是判斷 flag 在虛擬機中的映射是否爲零值,如果爲零值即跳轉,而對於 if (flag == true) 判斷的實質也是在判斷 flag 在虛擬機中的映射是否爲整數 1 ,如果非整數 1 即跳轉。
四、探究 boolean 的掩碼處理
4.1 概述
Java 虛擬機中在將 boolean、byte、char 以及 short 的值存入字段或者數組單元時,Java 虛擬機會對其進行 掩碼操作。在讀取時,Java 虛擬機則會將其擴展爲 int 類型。也就是說,boolean、byte、char、short 這四種類型,在棧上佔用的空間和 int 是一樣的,和引用類型也是一樣的。因此,在 32 位的 HotSpot 中,這些類型在棧上將佔用 4 個字節;而在 64 位的 HotSpot 中,他們將佔 8 個字節。而對於 byte、char 以及 short 這三種類型的字段或者數組單元,它們在堆上佔用的空間分別爲一字節、兩字節,以及兩字節,也就是說,跟這些類型的值域相吻合 。
那到底什麼時候 Java 虛擬機會對其進行 掩碼操作 呢,下面我們就來驗證一下。
4.2 求證
// Foo.java
public class Foo {
static boolean boolValue; // 注意這裏的 boolValue 保存在靜態域中
public static void main(String[] args) {
boolValue = true;
if (boolValue) System.out.println("Hello, Java!");
if (boolValue == true) System.out.println("Hello, JVM!");
}
}
$ javac Foo.java
$ java Foo
$ java -cp /path/to/asmtools.jar org.openjdk.asmtools.jdis.Main Foo.class > Foo.jasm.1
$ awk 'NR==1,/iconst_1/{sub(/iconst_1/, "iconst_2")} 1' Foo.jasm.1 > Foo.jasm
$ java -cp /path/to/asmtools.jar org.openjdk.asmtools.jasm.Main Foo.jasm
$ java Foo
$ javac Foo.java
$ java -cp /path/to/asmtools.jar org.openjdk.asmtools.jdis.Main Foo.class > Foo.jasm.1
$ awk 'NR==1,/iconst_1/{sub(/iconst_1/, "iconst_3")} 1' Foo.jasm.1 > Foo.jasm
$ java -cp /path/to/asmtools.jar org.openjdk.asmtools.jasm.Main Foo.jasm
$ java Foo
這次用來求證的命令行操作與上次相似,包括下面這兩步:
- 首先通過 javac 正常編譯 Foo.java 文件,然後將字節碼文件轉換爲 JASM 語法格式文件,並通過 awk 命令將其中的 iconst_1 替換爲 iconst_2 ,然後再通過 AsmTools 將其轉換爲字節碼文件,並通過 java 命令運行該文件;
- 其次再通過 javac 正常編譯 Foo.java 文件,然後將字節碼文件轉換爲 JASM 語法格式文件,並通過 awk 命令將其中的 iconst_1 替換爲 iconst_3 ,然後再通過 AsmTools 將其轉換爲字節碼文件,並通過 java 命令運行該文件;
命令運行後的結果如下圖所示:
4.3 解析
通過上述命令的運行我們可以發現一個很有趣的現象,首先當我們將 boolean 變量按照上一章節中的方法進行修改時(將其由 iconst_1 替換爲 iconst_2)會發現這次沒有任何輸出,而當我們將其由 iconst_1 替換爲 iconst_3 時卻同時打印了 Hello, Java! 和 Hello, JVM! ,說明 Java 虛擬機對其進行了掩碼操作,且掩碼操作是取其最低位,因此當其值爲 2 時取其最低位爲 0 ,而當其值爲 3 時取其最低位爲 1 ,所以當其值爲 iconst_2 時兩個判斷都無法通過,而當其值爲 iconst_3 時可以同時通過兩個判斷。
總結來說在上章實例代碼中的 boolean 變量是 非靜態域變量 ,而這裏的示例代碼則是將 boolean 保存在靜態域中,且指定了其類型爲 ‘Z’(boolean),當修改其值爲 2 時取最低位爲 0,而當修改爲 3 時取最低位爲 1 ,因此說明 boolean 的掩碼處理是取最低位的 。
五、內容總結
5.1 總結
- 在 Java 虛擬機規範中,boolean 類型則被映射成 int 類型 。具體來說,true 被映射爲整數 1,而 false 被映射爲整數 0 。同時這個編碼規則約束了 Java 字節碼的具體實現。
- 當將 boolean 保存在 靜態域 中,且指定了其類型爲 ‘Z’(boolean)時,此時 Java 虛擬機會對其進行掩碼操作,且 boolean 的掩碼處理是取最低位的 。