通過字節碼分析 Java 語言和 Java 虛擬機如何看待 boolean 類型

一、概述

  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 運行結果

pic

三、探究 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 文件中;
  • 使用 Linuxawk 命令在 Foo.jasm.1 文件中查找字符串 “iconst_1” 將其替換爲 “iconst_2” 並將替換後的文件內容輸出至 Foo.jasm 文件中;
  • 使用 AsmToolsJASM 語法文件 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) 在字節碼中分別被翻譯爲 ifeqif_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 命令運行該文件;

  命令運行後的結果如下圖所示:

pic2

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 的掩碼處理是取最低位的
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章