为什么short、byte会被提升为int,及基本类型的真实大小

  Java中,short 、byte、char 类型的数据在做运算的时候,都会默认提升为 int,如下面的代码,需要将等于号右边的强制转为 short 才可以通过编译。

1
2
3
4
5
6
7
public static void main(String[] args) {
short a = 1;
short b = 2;
a = a + b; // 编译不过
short c = a + b; // 编译不过
short d = (short) (a+b); // 编译通过
}

  为什么两个 short 相加会变成 int,有的解释说,两个 short 相加可能溢出,所以用 int 来接就不会溢出,那这样的话,两个 int 相加岂不应该是 long 类型吗?其实本质的原因要从字节码开始讲起。

本文出现了一些字节码指令,如果想详细查看,请参考Java虚拟机规范 Chapter 6. The Java Virtual Machine Instruction Set

为什么short、byte会被提升为int?

  Java虚拟机的指令由一个字节长度的、代表着某种特定操作含义的数字(称为操作码,Opcode)以及跟随其后的零至多个代表次操作所需参数(称为操作数,Operands)而构成。

  Java虚拟机的指令集中的大多数都对它们执行的操作的数据类型进行编码,例如 iload 指令,是将一个局部变量加载到操作栈,且这个局部变量必须是 int 类型,每一个指令都是只能接受对应类型的数据的。

  但由于操作码的长度为一个字节,这意味着指令集的操作码总数不可能超过256条,这也为设计包含数据类型的操作码带来了很大压力:如果每一种与数据类型相关的指令都支持Java虚拟机所有运行时数据类型的话,那指令的数量就会超出一个字节所能表示的数量范围了。

  根据下表(出自 Table 2.11.1-A. Type support in the Java Virtual Machine instruction set),可以发现大部分指令都没有支持 byte、char 和 short 类型,甚至没有任何指令支持 boolean 类型。因此如果要对两个 short 类型的数字相加,那只能转成 int,再使用 iadd 命令相加,然后再转成 short 咯

Java虚拟机指令集中的类型支持

基本类型有多大?

byte、short、char 的大小

常言道,byte 占 1 字节,short、char 两字节等等,事实真的如此吗?

举个例子,有如下代码

1
2
3
4
5
6
7
public class Test {
public static void main(String[] args) {
short a = 100;
short b = 200;
short c = (short) (a + b);
}
}

javac Test.java编译文件,javap -verbose Test查看 Class 文件内容,摘取main方法的信息:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=2, locals=4, args_size=1
0: bipush 100 // 由于~128-127,编译时就转为了byte类型,所以使用 bipush 将 byte 类型的数据100推入操作数栈
2: istore_1 // 栈顶int数值存入第2局部变量
3: sipush 200 //将 short 类型的数据200推入操作数栈
6: istore_2 // 栈顶int数值存入第3局部变量
7: iload_1 // 将第2局部变量入栈
8: iload_2 // 将第3局部变量入栈
9: iadd // 将栈顶的两个数int出栈并将相加结果入栈
10: i2s // 将栈顶的int出栈,转为short再入栈
11: istore_3 // 栈顶int数值存入第4局部变量
12: return

指令中使用了 bipush 来操作 byte类型的数据,查看该指令的描述(sipush也类似):

The immediate byte is sign-extended to an int value. That value is pushed onto the operand stack.
byte立刻被带符号地扩展为int值。该值被推送到操作数堆栈上。

bipush的描述

我们再查看一个用到的i2s的命令 jvms-6.5.i2s,它是这样描述的

The value on the top of the operand stack must be of type int. It is popped from the operand stack, truncated to a short, then sign-extended to an int result. That result is pushed onto the operand stack.
翻译过来大致是:
操作数堆栈顶部的值必须是int类型。它从操作数栈中弹出,截断为short,然后符号扩展为int结果。结果被推送到操作数堆栈上。

  所以从上面可以看出:在编译期或运行期 , short 类型将被带符号扩展为 int 类型,其它几个基本类型也一样。因此,大多数对于 boolean、byte、char 和 short 类型数据的操作,实际都提升为 int ,并使用 int 作为运算类型,所以 它们都占 4字节。实际上,虚拟机规范也只有 4字节 和 8字节类型(long、float), boolean、char、short 都是占了 4字节。

  这有没有让你想起Java栈上分配的最小单位 slot 就是 4 字节?就算是byte在栈上也是占用一个slot。

The Java stack is a last-in, first-out stack of 32-bit slots. Because each slot in the stack occupies 32 bits, all local variables occupy at least 32 bits. Local variables of type long and double, which are 64-bit quantities, occupy two slots on the stack. Local variables of type byte or short are stored as local variables of type int, but with a value that is valid for the smaller type. For example, an int local variable which represents a byte type will always contain a value valid for a byte (-128 <= value <= 127).
请参考: Bytecode basics

  当然了,slot为4字节也有其它原因,如在设计虚拟机时,主要考虑的是 32位体系,32位系统使用 4 字节是最节省,因为 CPU 只能 32位32位的寻址。

  那我们平时所说 short 是 2 字节的岂不是错误的?并不完全是,对于在栈上(局部变量)的 byte、char、short 类型的数据,在内存中的确会占 4 字节,但这对于(数组)对象来说并不适用。

  回首表2.11.1-A,byte类型只支持四种操作,大部分操作都需要转为int,那byte类型存在的意义是什么呢?反正都要占用一个slot,不如全部用int,其实就是因为数组对象的存在,例如其中两个指令 baload和bastore 就是操作数组的。

  虽然一个byte类型的变量只需要一个字节就能够表示了,但到了栈帧上(局部变量)却占4字节。如果byte数组对象中的每个元素也都占4字节,那就会浪费大量的空间。由于在对象数组中,元素都是分配在堆中的,栈上和堆上分配的机制不一样,在堆中每个元素可以被压缩成 1 字节(short数组每个元素压缩成2字节)。不过当你取出byte数组中的一个元素时,放入栈帧上时,又会被转成int,占用4字节。

  总结一下:byte、short、char等类型的数据当做局部变量使用时,实际也占用一个slot的大小,即4字节,但在数组中可以优化,byte 数组每个元素占 1 字节, char、short 数组各个元素占 2 字节。

参考stackoverflow Size of a byte in memory - Java,注意标注高亮的部分。
更多对基本类型的描述,可以查看Primitive Data Types


支持更少的 boolean

  说完byte、char、short,我们再来看看对于 boolean 的描述,摘取部分信息 2.3.4. The boolean Type

Although the Java Virtual Machine defines a boolean type, it only provides very limited support for it. There are no Java Virtual Machine instructions solely dedicated to operations on boolean values. Instead, expressions in the Java programming language that operate on boolean values are compiled to use values of the Java Virtual Machine int data type.

The Java Virtual Machine does directly support boolean arrays. Its newarray instruction (§newarray) enables creation of boolean arrays. Arrays of type boolean are accessed and modified using the byte array instructions baload and bastore (§baload, §bastore).

In Oracle’s Java Virtual Machine implementation, boolean arrays in the Java programming language are encoded as Java Virtual Machine byte arrays, using 8 bits per boolean element.

翻译大概如下:

尽管Java虚拟机定义了一种 boolean 类型,但对它的提供支持非常有限,没有专门的虚拟机指令用来操作 boolean 类型。但是,对于有 boolean 值参与运行的表达式,都会被编译成 int 类型的数据。

虚拟机直接支持了 boolean 数组,它使用newarray指令来创建数组,并可以使用 baloadbastore 来访问和修改 boolean 类型的数组

在 Oracle 的Java虚拟机实现中, boolean 类型的数组被编码成和 byte类型的数组, 每个 boolean 元素使用 8 bit。

  因此,boolean 作为局部变量使用时,也占 4 字节,在数组中使用时,按虚拟机规范要占 1 字节。但boolean数组最终如何实现,还是要看各个虚拟机厂商是否遵守规范了。