在 Smalltalk 中,所有的值都是对象。因此,许多人认为它是一门纯粹的面向对象语言。Java 则不同,它引进了八个基本类型,用来支撑数值计算。Java 这么做主要是出于工程上的考虑,使用基本类型能够在执行效率以及内存使用两方面提升软件性能。
Java 虚拟机的 boolean 类型
在 Java 语言规范中,boolean
类型的值只有两种可能,分别使用符合true
和false
来表示。
在 Java 虚拟机规范中, boolean
类型则被映射成int
类型,具体来说,true
被映射为整数1,false
被映射为整数0。这个编码规则约束了 Java 字节码的具体实现。
Java 虚拟机规范同时也要求 Java 编译器遵守这个编码规则,并且使用整数相关的字节码来实现逻辑运算。当然,这个约束很容易绕开,除了汇编工具 AsmTools
外,还有许多可以修改字节码的 Java 库,比如ASM
Java 的基本类型
类型 | 值域 | 默认值 | 虚拟机内部符号 |
---|---|---|---|
boolean | {false, true} | false | Z |
byte | [-128, 127] | 0 | B |
short | [-32768, 32767] | 0 | S |
char | [0, 65535] | ‘\u000’ | C |
int | [-2^31, 2^31-1] | 0 | I |
long | [-2^63, 2^63-1] | 0L | J |
float | ~[-3.4E38, 3.4E38] | +0.0F | F |
double | ~[-1.8E308, 1.8E308] | +0.0D | D |
byte
、short
、int
、long
、float
以及double
的值域依次扩大,而且前面的值被后面的值域所包含。因此从前面的基本类型转换至后面的基本类型,无需强制转换。- 它们的默认值看起来都不一样,但在内存中都是0。
- 这些基本类型中,
boolean
和char
是唯二的无符号类型。 - 声明为
byte
、char
以及short
的局部变量,能够存储它们取值范围的数值,但在正常使用 Java 编译器的情况下,生成的字节码会遵守 Java 虚拟机规范对编译器的约束。
Java 浮点数
Java 的浮点数采用 IEEE 754 浮点数格式。以 float
为例,浮点数类型通常有两个0,+0.0F以及-0.0F。前者在 Java 里是0,后者是符号位为1、其它位均为0的浮点数,在内存中等同于十六进制整数0x8000000。尽管它们的内存数值不同,但是在 Java 中+0.0F==-0.0F 会返回真。
浮点数中的正无穷:任意正浮点数除以 +0.0F 得到的值,在内存中等同于0x7F800000;
浮点数中的负无穷:任意正浮点数除以 -0.0F 得到的值,在内存中等同于0xFF800000。
标准 NaN(Not-a-Number):通过+0.0F/+0.0F 计算得到,在内存中为0x7FC00000;
非标准 NaN(Not-a-Number):0x7F800001、0x7FFFFFF、0xFF800001、0xFFFFFFFF等
NaN 和其它浮点数相比较,除了!=
始终返回true
外,所有其它比较结果均会返回false
。
1 | public static void main(String[] args) { |
Java 基本类型的大小
Java 虚拟机每调用一个 Java 方法,便会创建一个栈帧。为了方便解释,这里只讨论供解释器使用的解释栈帧(Interpreted frame)。该栈帧有两个主要组成部分,分别是局部变量区以及字节码的操作数栈。这里的局部变量是广义的,除了普通意义下的局部变量外,它还包含实例方法的”this指针”以及方法所接收的参数。
在 Java 虚拟机规范中,局部变量等价于一个数组,并且可以用正整数来索引。除了 long、double 值需要用两个数组单元来存储外,其他基本类型以及引用类型的值均占用一个数组单元。即 boolean、byte、char、short 这四种类型,在栈上占用的空间和 int 一样,和引用类型也一样。因此,在32位的HotSpot 中,这些类型将占用4个字节,而在64位的 HotSpot 中,他们将占用8个字节。(int 所包含的数据是4字节,由于 JVM 栈的实现方式,它们实际使用的内存可能占用得更多)
当然这种情况仅仅存在于局部变量,并不会出现在存储于堆上的字段或者数组元素。对于 byte、char以及 short 这三种类型的字段或数组单元,它们在堆上占用的空间分别为1字节、2字节以及2字节。(变长数组不好控制,所以选择浪费一些空间,以便访问时直接通过下标来计算地址)
当我们将一个 int 类型的值,存储到这些类型的字段或数组时,相当于做了一次隐式的掩码操作。举例来说,当我们把0xFFFFFFFF(-1)存储到一个声明为 char 类型的字段里时,由于该字段仅占用1字节,所以高两位的字节便会被截取掉,最终存入“\uFFFF”。
boolean 和 boolean 数组比较特殊,在 HotSpot 中,boolean 字段占用一字节,而boolean 数组直接使用 byte 数组来实现。为了保证堆中的 boolean 值是合法的,HotSpot 在存储时显式进行掩码操作,即只取最后一位的值存入 boolean 字段或数组中。
加载
Java 虚拟机的算数运算几乎全部依赖于操作数栈。也就是说,我们需要将堆中的 boolean、byte、char 以及 short 加载到操作数栈上,而后将栈上的值当成int类型来运算。
对于 boolean、char这两类无符号类型来说,加载伴随着零扩展,例如:在加载时 char 的值会被复制到 int 类型的低二字节,而高二字节会用0来填充。
对于byte、short这两个类型来说,加载伴随着符号扩展,例如:在加载时,short 值同样会被复制到 int 类型的低二字节。如果该 short 值为非负数,即最高位为0,那么该 int 类型的值的高二字节会用0来填充,否则用1来填充。
其他
ASM 是字节码工程包,它提供了字节码抽象的工具,允许用 Java 代码来生成或更改字节码。JDK里也会使用 ASM 来生成一些适配器什么的。
Unsafe 就是一些不被虚拟机控制的内存操作的合集,需要根据 API 来了解。
CAS 可以理解为原子性的写操作,概念来自于底层 CPU 指令。Unsafe 提供了一些 cas 的 Java 接口,在即时编译器中我们会将对这些接口的调用替换成具体的 CPU 指令
测试 NaN
1 | public static void main(String[] args) { |
测试结果
1 | nan1 raw: 7fc00000 |
如果不使用 raw 方法进行 NaN 的转换,采用 0.0f/0.0f
或 先定义变量f = 0.0f
再进行f/f
的计算,最终结果是一样的,但是如果使用 raw 方法进行转换,结果会有所不同,如:nan1 raw: 7fc00000
和nan2 raw: ffc00000
,原因是前者是 Java编译器给出的,后者是 CPU 用 DIVSS 指令计算出来的,跟具体的 CPU 实现有关系。由于都是 NaN,理论上哪个值都是正确的。