JVM 201 - Boolean


学习笔记,非原创,推荐配合「JVM 101 - 内存管理」一文阅读。

运行下面这段代码,显然会输出 AB
常情况下编译器不会接受将 2 这类数值赋值给 boolean 变量,但借助 asmtools 我们可以将 2 赋值给 flag 变量,会输出什么呢?如果将 flag 赋值为 3 呢?你会发现前者没有输出,后者输出为 AB

public class Foo {
    public static void main(String[] args) {
        boolean flag = true;
        if (flag) {
            System.out.print("A");
        }
        if (flag == true) {
            System.out.print("B");
        }
    }
}

要知道这两个问题的答案,我们得知道在 JVM 中boolean 类型的变量是如何表示的,以及在这两个 if 语句中到底进行了怎样的判断。

1. 深入 JVM boolean

在命令行中输入 javac Foo.java && javap -c Foo,得到反编译的字节码如下。

Compiled from "Foo.java"
public class Foo {
  public Foo();
    Code:
       0: aload_0
       1: invokespecial #1                  // Method java/lang/Object."<init>":()V
       4: return

  public static void main(java.lang.String[]);
    Code:
       0: iconst_1
       1: istore_1
       2: iload_1
       3: ifeq          14
       6: getstatic     #2                  // Field java/lang/System.out:Ljava/io/PrintStream;
       9: ldc           #3                  // String A
      11: invokevirtual #4                  // Method java/io/PrintStream.print:(Ljava/lang/String;)V
      14: iload_1
      15: iconst_1
      16: if_icmpne     27
      19: getstatic     #2                  // Field java/lang/System.out:Ljava/io/PrintStream;
      22: ldc           #5                  // String B
      24: invokevirtual #4                  // Method java/io/PrintStream.print:(Ljava/lang/String;)V
      27: return
}

在 main 方法中,第 0-2 行完成了将一个 int 类型的常量赋值给了第一个变量 flag 的操作。所以 在JVM 中 boolean 类型的变量是用数字 0 和 1 来表示的。false 用 0 表示,true 用 1 表示

if 助记符

  • ifeq:当栈顶 int 型数值等于 0 时跳转(不执行语句块中的代码)。
  • if_icmpne:比较栈顶两int型数值大小,当结果不相等时跳转。

接下来看 if (flag) 逻辑。此时的栈顶 int 型数值就是刚刚被赋值给 flag 的 1,所以在这里 ifeq 14 的意思就是当 flag 等于 0 的时候跳转到 14 行,由于第 14 已经不属于 if 语句的范围了,所以这里的跳转是不执行 if 语句的意思。翻译成人话就是:if (flag) 判断 flag 的值,当 flag 值等于 0 就不执行 if 中的语句

接下来继续看第二个判断语句 if (flag == true) 语句,是在字节码的 14-24 行。前面两行是将一个 int 类型的数值 1 和 flag 变量推送到栈顶,可以理解为把接下来将要进行比较的 true 放入将要进行比较的一个“容器”。if_icmpne 助记符比较这两个数值,如果他们相等,执行 if 语句的内容。

2. 解释

2.1. 为什么 JVM 可以操作数值类型的 booolean?

在 JVM 的眼里,并没有这么多的数据类型,对于 boolean 、byte 、short 和 char,在编译期都会变成 int 类型,JVM 也仅仅只对 int 提供了最完整的操作码,其他类型数据的操作,都是使用相应的 int 类型的操作码进行操作。

那么 JVM 为什么没有给每种数据类型都配置完整的操作码呢?

JVM 操作码的长度为一个字节,所以字节码指令集的操作码总数不可能超过 256 条。这么做是为了尽可能获得短小精干的字节码,字节码指令流都是单字节对齐的,数据量小,传输效率高。当然,这么做的代价就是你不可能设计出一套面向所有数据类型都完整的操作码。如果每一种数据结构都要得到 Java 虚拟机的字节码指令的支持的话,那么指令的数量将远远超过 256 种。所以,这也给指令集的设计带来了麻烦。最终权衡的结果就是,只对有限的类型提供完整的指令。大部分的指令都没有支持 byte、 char 和 short,boolean 则更惨,没有任何指令支持 boolean 类型。对于这些不支持的指令类型,一律使用 int 的相关指令代替。

2.2. 为什么给 flag 赋值 2, 3 会得到不同结果?

Java 虚拟机每调用一个 Java 方法,便会创建一个栈帧,这里我们讨论供解释器使用的解释栈帧(interpreted frame),它有两个主要的组成部分:局部变量区字节码的操作数栈

局部变量区等价于一个数组,并且可以用正整数来索引。除了 long、double 值需要用两个数组单元来存储之外,其他基本类型以及引用类型的值均占用一个数组单元。也就是说,boolean、byte、char、short 这四种类型,在栈上占用的空间和 int 是一样的,和引用类型也是一样的。因此,在 32 位的 HotSpot 中,这些类型在栈上将占用 4 个字节;而在 64 位的 HotSpot 中,他们将占 8 个字节。

注意
这种情况仅存在于局部变量,而并不会出现在存储于堆中的字段或者数组元素上。对于byte、char 以及 short 这三种类型的字段或者数组单元,它们在堆上占用的空间分别为 1 字节、2 字节,和 2 字节,也就是说,跟这些类型的值域相吻合。

重点来了:当我们将一个 int 类型的值,存储到这些类型的字段或数组时,相当于做了一次隐式的掩码操作。举例来说,当我们把 0xFFFFFFFF(-1)存储到一个声明为 char 类型的字段里时,由于该字段仅占两字节,所以高两位的字节便会被截取掉,最终存入“\uFFFF”。

回到本文开头的问题,如果我们将这些整数都转化为二进制,即 2=00103=0011。当我们将一个 int 类型的值存储到 boolean 类型的局部变量时,JVM 通过掩码只会取此整数值二进制的最后一位,所以当二进制末尾为 0 时,无输出,当二进制末尾为 1 时,输出 AB

3. Takeaways

  1. Boolean 其实是被当做 int 值处理的,true 表示 1,false 表示 0。
  2. 存储在局部变量区的 boolean、byte、char、short 会经历隐式的掩码操作;堆或数组中则不会。

4. 参考资料

  1. 「JVM」原始类型 boolean 在 JVM 中的讨论
  2. 深入拆解 Java 虚拟机

文章作者: Shane Tsui
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 Shane Tsui !

  目录