Java的基本数据类型在虚拟机中的实现-阿里云开发者社区

开发者社区> 开发与运维> 正文
登录阅读全文

Java的基本数据类型在虚拟机中的实现

简介: 前言 首先我们做个小测验,通过下面代码来看看Java语言和Java虚拟机对boolean类型有什么不同: public class Foo { public static void main(String[] args...

前言

首先我们做个小测验,通过下面代码来看看Java语言和Java虚拟机对boolean类型有什么不同:

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!");
   }
}

我们编译执行后的输出结果如下:

$ javac Foo.java 
$ java Foo
$ Hello, Java!
  Hello, JVM!

下面我们通过asmtools将虚拟机中flag的值改为2,我们再看看输出结果。

$ java -cp /Users/leiqi/Library/sdk/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 /Users/leiqi/Library/sdk/asmtools.jar  org.openjdk.asmtools.jasm.Main Foo.jasm
$ java Foo
$ Hello, Java!

这次我们看到输出结果跟上次不一样了。那么我们可能会想到:当一个boolean类型的值为2时,它究竟是true还是false?

下面我们详细分析一下它背后的逻辑。

Java虚拟机的boolean类型

首先,我们看看Java语言规范和Java虚拟机规范中分别怎么定义boolean类型。

在Java语言规范中boolean类型的值只有两种可能,true或者false。 但是这两者不能直接被虚拟机引用。

在Java虚拟机规范中,boolean类型的值被映射为int类型。true被映射为1,false被映射为0。这个编码映射规则约束了Java字节码的具体实现。也就是说:对于存储boolean数组的字节码 ,虚拟机需要保证存入的是1或0

Java虚拟机同时也要求Java编译器对应也得遵守这个规则,并且用整数相关的字节码来实现逻辑运算,以及基于boolean类型的条件跳转。这样一来,在编译而成的class文件中,除了字段和传入的参数外,基本看不出boolean类型的痕迹。

# Foo.main 编译后的字节码 
0: iconst_2 // 我们用 AsmTools更改了这一指令
1: istore_1
2: iload_1
3: ifeq 14 // 第一个 if 语句,即操作数栈上数值为 0 时跳转
6: getstatic java.lang.System.out
9: ldc "Hello, Java!"
11: invokevirtual java.io.PrintStream.println
14: iload_1
15: iconst_1
16: if_icmpne 27 // 第二个if语句,即操作数栈上两个数值不相同时跳转
19: getstatic java.lang.System.out
22: ldc "Hello, JVM!"
24: invokevirtual java.io.PrintStream.println
27: return

在前面的例子中,第一个if语句会被编译成条件跳转字节码ifeq,翻译成人话就是说,如果局部变量flag的值为0,那么不打印“Hello Java!”。

而第二个if语句则会编译为条件跳转字节码if_cmpne,也就是说,如果局部变量的值和整数1相等,则打印“Hello JVM!” ,否则跳过这句。

可以看到,Java 编译器的确遵守了相同的编码规则。当然,这个约束很容易绕开。除了我们之前用得到的AsmTools外,还有一些工具可以修改字节码的java库,如ASM等。

对于JAVA虚拟机来说,他看到的boolean类型,早已被映射为整数类型。因此,将原本声明为boolean类型的局部变量,赋值为除了0,1之外的整数值,在Java虚拟机看来是“合法”的。

在之前的例子中,经过编译器编译之后,java虚拟机看到的不是在问“flag是否非零”,而是变成在问“flag为几”。也就是第一个if语句变成:flag的值为0吗?第二个if语句则变成:flag的值是1吗?

如果约定俗成,flag只能为0/1,那么第二个if语句还是有意义的。但如果我们打破常规,flag的值为大于1,那么较真的Java虚拟机就会将第二个if语句判定为假。

Java的基本类型

除了上面提到的boolean类型外,Java的基本类型还包括byte、short、char、int、long、float以及double。

Java的基本类型都有对应的值域和默认值。可以看到,byte、short、int、long、float以及double的值域依次扩大,而且前面的值域被后面的值域所包含。因此,从前面的基本类型转换至后面的基本类型,无需强制转换。另外一点值得注意的是,尽管他们的默认值看起来不一样,但是在内存中都是0。

在这些基本类型中,boolean和char是唯二的无符号类型。在不考虑违反规范的情况下,boolean类型的取值范围是0或者1.char类型的取值范围是[0,65535]。通常我们可以认定char类型的值为负数。这种特性十分有用,比如说作为数组索引等。

在前面的例子中,我们能够将整数2存储到一个声明为boolean类型的局部变量中。那么,声明为byte,char以及short的局部变量,是否也能够存储超出它们范围的数值昵?

答案是可以的。而且,这些超出取值范围的数值依然会带来一些麻烦。比如说,声明为char类型的局部变量实际上有可能为负数。当然,在政策使用Java编译器的情况下,生成的字节码会遵守Java虚拟机规范对编译器的约束,因此你无须过分担心局部变量会超出它们的取值范围。

Java的浮点类型彩印IEEE 754 浮点数格式。以float为例,浮点类型通常有两个0,+0.0F以及-0.0F。

前者在Java里是0,后者是符号位为1,其他位均为0的浮点数,在内存中等同于十六进制整数0x8000000(即-0.0F可通过Float.intBitsToFloat(0x8000000)求得)。尽管它们的内存数值不同,但是在Java中 +0.0F == -0.0F会返回为真。

在有了+0.0F 和 -0.0F这两个定义以后,我们便可以定义浮点数中的正无穷及负无穷。正无穷就是任意正浮点数(不包括+0.0F)除以+0.0F得到的值,而负无穷就是任意负浮点数(不包括-0.0F)除以-0.0F得到的值。 在Java中,正无穷和负无穷是有确切的值,在内存中分别等同于十六进制整数0x7F800000 和 0xFF800000。

[0x7F800001, 0x7FFFFFFF] 和 [0xFF800001, 0xFFFFFFFF] 对应的都是NaN(Not-a-Number) 。当然,一般我们计算得出的NaN,比如说通过+0.0F/+0.0F,在内存中应为0x7FC00000。这个数字,我们都称之为标准的NaN,而其他的我们称之为不标准的NaN。

NaN有一个有趣的特性:除了"!="始终返回true之外,所有其它比较结果都会返回false。

举例来说,

  • NaN < 1.0F -> false
  • NaN >= 1.0F -> false
  • f != NaN -> true (f为任意浮点数)
  • f == NaN -> false (f为任意浮点数)

因此,我们在程序里做浮点数比较时,需要考虑上述特性。

Java基本类型的大小

前面有提到 Java虚拟机每调用一个方法,便会创建一个栈帧 为了方便理解,这里我只讨论供解释器使用的解释栈帧(interpreted frame)。

这种栈帧有两个主要的组成部分,分别是局部变量区,以及字节码的操作数栈。这里的局部变量是广义的,除了普遍意义下的局部变量之外,它还包含实例方法的"this指针"以及方法所接受的参数。

在Java虚拟机规范中,局部变量区等价于一个数组,并且可以用正整数来索引。除了long、double值需要用两个赎罪单元来存储外,其它基本类型以及引用类型的值均占用一个数组单元。

也就是说,boolean、byte、char、short 这四种类型,在栈上占用的空间和 int 是一样的,和引用类型也是一样的。因此,在 32 位的 HotSpot 中,这些类型在栈上将占用 4 个字节;而在 64 位的 HotSpot 中,他们将占 8 个字节。

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

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

boolean 字段和 boolean 数组则比较特殊。在 HotSpot 中,boolean 字段占用一字节,而 boolean 数组则直接用 byte 数组来实现。为了保证堆中的 boolean 值是合法的,HotSpot 在存储时显式地进行掩码操作,也就是说,只取最后一位的值存入 boolean 字段或数组中。

讲完了存储,现在我来讲讲加载。Java 虚拟机的算数运算几乎全部依赖于操作数栈。也就是说,我们需要将堆中的 boolean、byte、char 以及 short 加载到操作数栈上,而后将栈上的值当成 int 类型来运算。

对于 boolean、char 这两个无符号类型来说,加载伴随着零扩展。举个例子,char 的大小为两个字节。在加载时 char 的值会被复制到 int 类型的低二字节,而高二字节则会用 0 来填充。

对于 byte、short 这两个类型来说,加载伴随着符号扩展。举个例子,short 的大小为两个字节。在加载时 short 的值同样会被复制到 int 类型的低二字节。如果该 short 值为非负数,即最高位为 0,那么该 int 类型的值的高二字节会用 0 来填充,否则用 1 来填充。

总结

1、boolean 类型在 Java 虚拟机中被映射为整数类型:“true”被映射为 1,而“false”被映射为 0。Java 代码中的逻辑运算以及条件跳转,都是用整数相关的字节码来实现的。

2、除 boolean 类型之外,Java 还有另外 7 个基本类型。它们拥有不同的值域,但默认值在内存中均为 0。这些基本类型之中,浮点类型比较特殊。基于它的运算或比较,需要考虑 +0.0F、-0.0F 以及 NaN 的情况。

3、除 long 和 double 外,其他基本类型与引用类型在解释执行的方法栈帧中占用的大小是一致的,但它们在堆中占用的大小确不同。在将 boolean、byte、char 以及 short 的值存入字段或者数组单元时,Java 虚拟机会进行掩码操作。在读取时,Java 虚拟机则会将其扩展为 int 类型。

4、将boolean 保存在静态域中,指定了其类型为’Z’,当修改为2时取低位最后一位为0,当修改为3时取低位最后一位为1。
则说明boolean的掩码处理是取低位的最后一位

版权声明:本文内容由阿里云实名注册用户自发贡献,版权归原作者所有,阿里云开发者社区不拥有其著作权,亦不承担相应法律责任。具体规则请查看《阿里云开发者社区用户服务协议》和《阿里云开发者社区知识产权保护指引》。如果您发现本社区中有涉嫌抄袭的内容,填写侵权投诉表单进行举报,一经查实,本社区将立刻删除涉嫌侵权内容。

分享:
开发与运维
使用钉钉扫一扫加入圈子
+ 订阅

集结各类场景实战经验,助你开发运维畅行无忧

其他文章
最新文章
相关文章