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的掩码处理是取低位的最后一位

相关文章
|
5月前
|
Java
当Java数据类型遇上“爱情”,会擦出怎样的火花?
当Java数据类型遇上“爱情”,会擦出怎样的火花?
67 1
|
24天前
|
存储 缓存 安全
Java中的数据类型
Java语言提供了八种基本类型,分为4类8种:六个数值型(四个整数型byte、short、int、long,两个浮点型float、double)、一个字符型char和一个布尔型boolean。每种基本类型有固定的位数、取值范围及默认值。此外,还存在`void`类型,但无法直接操作。基本类型支持隐式和显式类型转换,并有对应的包装类如`Integer`、`Double`等,用于在需要对象的场景中使用。包装类支持自动装箱与拆箱机制,简化了基本类型与引用类型的转换,但需要注意性能和空指针异常等问题。
Java中的数据类型
|
4月前
|
Java
java基础(8)数据类型的分类
Java数据类型分为基本数据类型(8种)和引用数据类型。基本类型包括byte, short, int, long, float, double, boolean, char。每种类型有固定占用空间大小,如int占用4字节。字符编码如ASCII和Unicode用于将文字转换为计算机可识别的二进制形式。
97 2
|
5月前
|
Java 程序员
Java数据类型:为什么程序员都爱它?
Java数据类型:为什么程序员都爱它?
60 1
|
1月前
|
存储 监控 算法
深入探索Java虚拟机(JVM)的内存管理机制
本文旨在为读者提供对Java虚拟机(JVM)内存管理机制的深入理解。通过详细解析JVM的内存结构、垃圾回收算法以及性能优化策略,本文不仅揭示了Java程序高效运行背后的原理,还为开发者提供了优化应用程序性能的实用技巧。不同于常规摘要仅概述文章大意,本文摘要将简要介绍JVM内存管理的关键点,为读者提供一个清晰的学习路线图。
|
2月前
|
Java
Java基础之数据类型
Java基础之数据类型
23 6
|
2月前
|
Java
在Java中如何将基本数据类型转换为String
在Java中,可使用多种方法将基本数据类型(如int、char等)转换为String:1. 使用String.valueOf()方法;2. 利用+运算符与空字符串连接;3. 对于数字类型,也可使用Integer.toString()等特定类型的方法。这些方法简单高效,适用于不同场景。
109 7
|
2月前
|
存储 缓存 Java
大厂面试必看!Java基本数据类型和包装类的那些坑
本文介绍了Java中的基本数据类型和包装类,包括整数类型、浮点数类型、字符类型和布尔类型。详细讲解了每种类型的特性和应用场景,并探讨了包装类的引入原因、装箱与拆箱机制以及缓存机制。最后总结了面试中常见的相关考点,帮助读者更好地理解和应对面试中的问题。
81 4
|
2月前
|
存储 消息中间件 NoSQL
使用Java操作Redis数据类型的详解指南
通过使用Jedis库,可以在Java中方便地操作Redis的各种数据类型。本文详细介绍了字符串、哈希、列表、集合和有序集合的基本操作及其对应的Java实现。这些示例展示了如何使用Java与Redis进行交互,为开发高效的Redis客户端应用程序提供了基础。希望本文的指南能帮助您更好地理解和使用Redis,提升应用程序的性能和可靠性。
47 1
|
3月前
|
存储 算法 Java
Java虚拟机(JVM)的内存管理与性能优化
本文深入探讨了Java虚拟机(JVM)的内存管理机制,包括堆、栈、方法区等关键区域的功能与作用。通过分析垃圾回收算法和调优策略,旨在帮助开发者理解如何有效提升Java应用的性能。文章采用通俗易懂的语言,结合具体实例,使读者能够轻松掌握复杂的内存管理概念,并应用于实际开发中。