文章目录
一、概述
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 的掩码处理是取最低位的 。