通过字节码分析 Java 语言和 Java 虚拟机如何看待 boolean 类型

3 篇文章 0 订阅

一、概述

  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 的掩码处理是取最低位的
  • 3
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值