Java的基本类型

Smalltalk 中,所有的值都是对象。因此,许多人认为它是一门纯粹的面向对象语言。Java 则不同,它引进了八个基本类型,用来支撑数值计算。Java 这么做主要是出于工程上的考虑,使用基本类型能够在执行效率以及内存使用两方面提升软件性能。

Java 虚拟机的 boolean 类型

在 Java 语言规范中,boolean类型的值只有两种可能,分别使用符合truefalse来表示。

在 Java 虚拟机规范中, boolean类型则被映射成int类型,具体来说,true被映射为整数1,false被映射为整数0。这个编码规则约束了 Java 字节码的具体实现。

Java 虚拟机规范同时也要求 Java 编译器遵守这个编码规则,并且使用整数相关的字节码来实现逻辑运算。当然,这个约束很容易绕开,除了汇编工具 AsmTools外,还有许多可以修改字节码的 Java 库,比如ASM

Java 的基本类型

类型值域默认值虚拟机内部符号
boolean{false, true}falseZ
byte[-128, 127]0B
short[-32768, 32767]0S
char[0, 65535]‘\u000’C
int[-2^31, 2^31-1]0I
long[-2^63, 2^63-1]0LJ
float~[-3.4E38, 3.4E38]+0.0FF
double~[-1.8E308, 1.8E308]+0.0DD
  1. byteshortintlongfloat以及double的值域依次扩大,而且前面的值被后面的值域所包含。因此从前面的基本类型转换至后面的基本类型,无需强制转换。
  2. 它们的默认值看起来都不一样,但在内存中都是0。
  3. 这些基本类型中,booleanchar是唯二的无符号类型。
  4. 声明为bytechar以及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
2
3
4
5
6
7
public static void main(String[] args) {
System.out.println(Float.floatToIntBits(0.0F)); //0
System.out.println(Float.floatToIntBits(-0.0F)); //-2147483648
System.out.println(Float.floatToIntBits(0.0F/0.0F)); //2143289344
System.out.println(0x7FC00000); //2143289344
System.out.println(Float.intBitsToFloat(0x7F800001)); //NaN
}

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
2
3
4
5
6
7
8
9
10
11
12
13
14
public static void main(String[] args) {
float nan1 = 0.0f/0.0f;
float f = 0.0f;
float nan2 = f/f;
double d = 0.0;
System.out.println("nan1 raw: " + Integer.toHexString(Float.floatToRawIntBits(nan1)));
System.out.println("nan1: " + Integer.toHexString(Float.floatToIntBits(nan1)));
System.out.println("nan2 raw: " + Integer.toHexString(Float.floatToRawIntBits(nan2)));
System.out.println("nan2: " + Integer.toHexString(Float.floatToIntBits(nan2)));
System.out.println("nan3 raw: " + Long.toHexString(Double.doubleToRawLongBits(0.0/0.0)));
System.out.println("nan3: " + Long.toHexString(Double.doubleToLongBits(0.0/0.0)));
System.out.println("nan4 raw: " + Long.toHexString(Double.doubleToRawLongBits(d/d)));
System.out.println("nan4: " + Long.toHexString(Double.doubleToLongBits(d/d)));
}

测试结果

1
2
3
4
5
6
7
8
9
10
nan1 raw: 7fc00000
nan1: 7fc00000
nan2 raw: ffc00000
nan2: 7fc00000
nan3 raw: 7ff8000000000000
nan3: 7ff8000000000000
nan4 raw: fff8000000000000
nan4: 7ff8000000000000

Process finished with exit code 0

如果不使用 raw 方法进行 NaN 的转换,采用 0.0f/0.0f 或 先定义变量f = 0.0f再进行f/f的计算,最终结果是一样的,但是如果使用 raw 方法进行转换,结果会有所不同,如:nan1 raw: 7fc00000nan2 raw: ffc00000,原因是前者是 Java编译器给出的,后者是 CPU 用 DIVSS 指令计算出来的,跟具体的 CPU 实现有关系。由于都是 NaN,理论上哪个值都是正确的。