极客时间已完结课程限时免费阅读

02 | Java的基本类型

02 | Java的基本类型-极客时间

02 | Java的基本类型

讲述:郑雨迪

时长13:27大小6.16M

如果你了解面向对象语言的发展史,那你可能听说过 Smalltalk 这门语言。它的影响力之大,以至于之后诞生的面向对象语言,或多或少都借鉴了它的设计和实现。
在 Smalltalk 中,所有的值都是对象。因此,许多人认为它是一门纯粹的面向对象语言。
Java 则不同,它引进了八个基本类型,来支持数值计算。Java 这么做的原因主要是工程上的考虑,因为使用基本类型能够在执行效率以及内存使用两方面提升软件性能。
今天,我们就来了解一下基本类型在 Java 虚拟机中的实现。
public class Foo {
public static void main(String[] args) {
boolean 吃过饭没 = 2; // 直接编译的话javac会报错
if (吃过饭没) System.out.println("吃了");
if (true == 吃过饭没) System.out.println("真吃了");
}
}
在上一篇结尾的小作业里,我构造了这么一段代码,它将一个 boolean 类型的局部变量赋值为 2。为了方便记忆,我们给这个变量起个名字,就叫“吃过饭没”。
赋值语句后边我设置了两个看似一样的 if 语句。第一个 if 语句,也就是直接判断“吃过饭没”,在它成立的情况下,代码会打印“吃了”。
第二个 if 语句,也就是判断“吃过饭没”和 true 是否相等,在它成立的情况下,代码会打印“真吃了”。
当然,直接编译这段代码,编译器是会报错的。所以,我迂回了一下,采用一个 Java 字节码的汇编工具,直接对字节码进行更改。
那么问题就来了:当一个 boolean 变量的值是 2 时,它究竟是 true 还是 false 呢?
如果你跑过这段代码,你会发现,问虚拟机“吃过饭没”,它会回答“吃了”,而问虚拟机“真(==)吃过饭没”,虚拟机则不会回答“真吃了”。
那么虚拟机到底吃过没,下面我们来一起分析一下这背后的细节。

Java 虚拟机的 boolean 类型

首先,我们来看看 Java 语言规范以及 Java 虚拟机规范是怎么定义 boolean 类型的。
在 Java 语言规范中,boolean 类型的值只有两种可能,它们分别用符号“true”和“false”来表示。显然,这两个符号是不能被虚拟机直接使用的。
在 Java 虚拟机规范中,boolean 类型则被映射成 int 类型。具体来说,“true”被映射为整数 1,而“false”被映射为整数 0。这个编码规则约束了 Java 字节码的具体实现。
举个例子,对于存储 boolean 数组的字节码,Java 虚拟机需保证实际存入的值是整数 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 "吃了"
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 "真吃了"
24: invokevirtual java.io.PrintStream.println
27: return
在前面的例子中,第一个 if 语句会被编译成条件跳转字节码 ifeq,翻译成人话就是说,如果局部变量“吃过饭没”的值为 0,那么跳过打印“吃了”的语句。
而第二个 if 语句则会被编译成条件跳转字节码 if_icmpne,也就是说,如果局部变量的值和整数 1 不相等,那么跳过打印“真吃了”的语句。
可以看到,Java 编译器的确遵守了相同的编码规则。当然,这个约束很容易绕开。除了我们小作业中用到的汇编工具 AsmTools 外,还有许多可以修改字节码的 Java 库,比如说 ASM [1] 等。
对于 Java 虚拟机来说,它看到的 boolean 类型,早已被映射为整数类型。因此,将原本声明为 boolean 类型的局部变量,赋值为除了 0、1 之外的整数值,在 Java 虚拟机看来是“合法”的。
在我们的例子中,经过编译器编译之后,Java 虚拟机看到的不是在问“吃过饭没”,而是在问“吃过几碗饭”。也就是说,第一个 if 语句变成:你不会一碗饭都没吃吧。第二个 if 语句则变成:你吃过一碗饭了吗。
如果我们约定俗成,每人每顿只吃一碗,那么第二个 if 语句还是有意义的。但如果我们打破常规,吃了两碗,那么较真的 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 得到的值。在 Java 中,正无穷和负无穷是有确切的值,在内存中分别等同于十六进制整数 0x7F800000 和 0xFF800000。
你也许会好奇,既然整数 0x7F800000 等同于正无穷,那么 0x7F800001 又对应什么浮点数呢?
这个数字对应的浮点数是 NaN(Not-a-Number)。
不仅如此,[0x7F800001, 0x7FFFFFFF]和[0xFF800001, 0xFFFFFFFF]对应的都是 NaN。当然,一般我们计算得出的 NaN,比如说通过 +0.0F/+0.0F,在内存中应为 0x7FC00000。这个数值,我们称之为标准的 NaN,而其他的我们称之为不标准的 NaN。
NaN 有一个有趣的特性:除了“!=”始终返回 true 之外,所有其他比较结果都会返回 false。
举例来说,“NaN<1.0F”返回 false,而“NaN>=1.0F”同样返回 false。对于任意浮点数 f,不管它是 0 还是 NaN,“f!=NaN”始终会返回 true,而“f==NaN”始终会返回 false。
因此,我们在程序里做浮点数比较的时候,需要考虑上述特性。在本专栏的第二部分,我会介绍这个特性给向量化比较带来什么麻烦。

Java 基本类型的大小

在第一篇中我曾经提到,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 来填充。

总结与实践

今天我介绍了 Java 里的基本类型。
其中,boolean 类型在 Java 虚拟机中被映射为整数类型:“true”被映射为 1,而“false”被映射为 0。Java 代码中的逻辑运算以及条件跳转,都是用整数相关的字节码来实现的。
除 boolean 类型之外,Java 还有另外 7 个基本类型。它们拥有不同的值域,但默认值在内存中均为 0。这些基本类型之中,浮点类型比较特殊。基于它的运算或比较,需要考虑 +0.0F、-0.0F 以及 NaN 的情况。
除 long 和 double 外,其他基本类型与引用类型在解释执行的方法栈帧中占用的大小是一致的,但它们在堆中占用的大小确不同。在将 boolean、byte、char 以及 short 的值存入字段或者数组单元时,Java 虚拟机会进行掩码操作。在读取时,Java 虚拟机则会将其扩展为 int 类型。
今天的动手环节,你可以观测一下,将 boolean 类型的值存入字段中时,Java 虚拟机所做的掩码操作。
你可以将下面代码中 boolValue = true 里的 true 换为 2 或者 3,看看打印结果与你的猜测是否相符合。
熟悉 Unsafe 的同学,可以使用 Unsafe.putBoolean 和 Unsafe.putByte 方法,看看还会不会做掩码操作。
public class Foo {
static boolean boolValue;
public static void main(String[] args) {
boolValue = true; // 将这个true替换为2或者3,再看看打印结果
if (boolValue) System.out.println("Hello, Java!");
if (boolValue == true) System.out.println("Hello, JVM!");
}
}
分享给需要的人,Ta购买本课程,你将得18
生成海报并分享

赞 49

提建议

上一篇
01 | Java代码是怎么运行的?
下一篇
03 | Java虚拟机是如何加载Java类的?
 写留言

精选留言(124)

  • 丨落灬小莫
    2018-07-23
    当替换为2的时候无输出 当替换为3的时候打印HelloJava及HelloJVM 猜测是因为将boolean 保存在静态域中,指定了其类型为'Z',当修改为2时取低位最后一位为0,当修改为3时取低位最后一位为1 则说明boolean的掩码处理是取低位的最后一位

    作者回复: 对的!

    共 11 条评论
    329
  • 追梦
    2018-10-21
    有个地方初看不易看懂,我来解释下: 作者一开始放的“吃没吃饭”的例子中boolean变量是局部变量,存放在Java方法栈的栈帧中的局部变量区,占据一个数据单元,无需做掩码;最后的例子中boolean变量是成员变量,存储在堆中的对象实例里,占有一个字节,且需要对最后一位做掩码
    共 7 条评论
    93
  • Kyle
    2018-07-23
    老师,文中看你说到: “也就是说,boolean、byte、char、short 这四种类型,在栈上占用的空间和 int 是一样的,和引用类型也是一样的。因此,在 32 位的 HotSpot 中,这些类型在栈上将占用 4 个字节;而在 64 位的 HotSpot 中,他们将占 8 个字节。” 但是我记得boolean在内存中占1字节,char占2字节,这里是什么个意思?
    展开

    作者回复: 你说的是在堆里的情况。在解释器栈上是不一样的。至于原因吗,主要是变长数组不好控制,所以就选择浪费一些空间,以便访问时直接通过下标来计算地址。

    共 2 条评论
    64
  • 金蝉子
    2018-07-23
    老师可以讲下ASM、Unsafe和CAS的底层原理吗?这块儿一直是个拦路虎,谢谢!

    作者回复: ASM你指的是那个字节码工程包吗?是的话那就是一个提供了字节码抽象的工具,允许用Java代码来生成或者更改字节码。JDK里也会用到ASM,用来生成一些适配器什么的。我印象中代码覆盖工具JaCoCo也是用ASM来实现的。 Unsafe就是一些不被虚拟机控制的内存操作的合集。具体想要了解哪个API? CAS可以理解为原子性的写操作,这个概念来自于底层CPU指令。Unsafe提供了一些cas的Java接口,在即时编译器中我们会将对这些接口的调用替换成具体的CPU指令。

    共 3 条评论
    55
  • 曲东方
    2018-07-23
    Unsafe.putBoolean和Unsafe.puByte是native实现 putBoolean和putByte也是通过宏SET_FIELD模板出的函数 #define SET_FIELD(obj, offset, type_name, x) \ oop p = JNIHandles::resolve(obj); \ *(type_name*)index_oop_from_field_offset_long(p, offset) = truncate_##type_name(x) unsafe.cpp中定义宏做truncate #define truncate_jboolean(x) ((x) & 1) #define truncate_jbyte(x) (x) #define truncate_jshort(x) (x) #define truncate_jchar(x) (x) #define truncate_jint(x) (x) #define truncate_jlong(x) (x) #define truncate_jfloat(x) (x) #define truncate_jdouble(x) (x) 综上:unsafe.Put*不会对值做修改 ------------------------------------------------------------------------------------ getBoolean和getByte也是通过宏GET_FIELD模板出的函数 #define GET_FIELD(obj, offset, type_name, v) \ oop p = JNIHandles::resolve(obj); \ type_name v = *(type_name*)index_oop_from_field_offset_long(p, offset) 综上,unsafe.Get*不会对值做修改 ------------------------------------------------------------------------------------ 验证: unsafe.putByte(foo, addr, (byte)2); // 设置为: 2 System.out.println(unsafe.getByte(foo, addr)); // 打印getByte: 2 System.out.println(unsafe.getBoolean(foo, addr)); // 打印getBoolean: true unsafe.putByte(foo, addr, (byte)1); // 设置为: 1 System.out.println(unsafe.getByte(foo, addr)); // 打印getByte: 1 System.out.println(unsafe.getBoolean(foo, addr)); // 打印getBoolean: true ------------------------------------------------------------------------------------ 疑问: if(foo.flag)判断,使用getfield Field flag:"Z",执行逻辑等于:0 != flag if(foo.getFlag())判断,使用invokevirtual Method getFlag:"()Z",执行逻辑等于: 0 != ((flag) & 1) 求大神帮忙解答 -------------------------- 附getFlag jasm码: public Method getFlag:"()Z" stack 1 locals 1 { aload_0; getfield Field flag:"Z"; ireturn; } https://gist.github.com/qudongfang/49635d86882c03e49cff2b0f7d833805
    展开

    作者回复: 研究得非常深入! Unsafe.putBoolean会做掩码,另外方法返回也会对boolean byte char short进行掩码

    共 2 条评论
    38
  • 匿名小板凳
    2018-09-12
    这节看的很吃力,对什么掩码,子码,反码,补码都换给大学老师了。
    33
  • Geek_dde3ac
    2018-07-23
    你好,在内存中都是0,那么是如何区别是哪种类型数据的呢?

    作者回复: 内存中是不做区分的。Java程序想要把它解读成什么类型,它就是什么类型。

    共 2 条评论
    32
  • 落叶飞逝的恋
    2018-07-24
    其实那个boolean的true虚拟机里面为1,也就是if(true==吃了没)其实可以替换成if(1==2)这样理解吧

    作者回复: 对的!

    19
  • dong
    2018-07-27
    感觉"吃饭了"例子,弄得有点饶了。也有些地方语句的起承转合不是很通顺,个人理解。

    作者回复: 谢谢建议

    17
  • 别处
    2018-07-24
    以下两个引至本文内容: 1、在 Java 虚拟机规范中,boolean 类型则被映射成 int 类型。 2、在 HotSpot 中,boolean 字段占用一字节, 问题:一个是int类型,一个是一个字节(32位系统的话就是byte类型),是没讲透还是错误?

    作者回复: 多谢指出。严格来说,前者指的是计算的时候被映射成int类型,后者指的是存储到堆中是一个字节。

    15
  • Invincible、
    2018-11-29
    为什么我不能让boolvalue=2或者3……

    作者回复: 因为javac不支持这种操作,它把boolean是用int实现的这种虚拟机的实现细节给隐藏起来了,从而使得在语言层面没有这种会引起歧义的值。

    12
  • life is short, e...
    2018-09-14
    老师你好,我刚来订阅,所以才开始看。 有一个疑问,您的原文“因此,在 32 位的 HotSpot 中,这些类型在栈上将占用 4 个字节;而在 64 位的 HotSpot 中,他们将占 8 个字节。”。但是有一句话,java一次编译,到处运行。计算机位数不一样的话,导致一样类型的size不一样,还可以到处运行吗?这里指的到处运行,是不是需要同位啊?比如32位的编译只能在32位的机器上运行,64只能在64的上运行。能互相兼容运行嘛?
    展开
    共 6 条评论
    12
  • andy
    2018-09-13
    我替换成2和3,都只能打印出一个Hello Java为什么呢?下面是AsmTools反编译代码 super public class Foo version 52:0 { static Field boolValue:Z; public Method "<init>":"()V" stack 1 locals 1 { aload_0; invokespecial Method java/lang/Object."<init>":"()V"; return; } public static Method main:"([Ljava/lang/String;)V" stack 2 locals 1 { iconst_2; putstatic Field boolValue:"Z"; getstatic Field boolValue:"Z"; ifeq L18; getstatic Field java/lang/System.out:"Ljava/io/PrintStream;"; ldc String "Hello, Java!"; invokevirtual Method java/io/PrintStream.println:"(Ljava/lang/String;)V"; L18: stack_frame_type same; getstatic Field boolValue:"Z"; iconst_1; if_icmpne L33; getstatic Field java/lang/System.out:"Ljava/io/PrintStream;"; ldc String "Hello, JVM!"; invokevirtual Method java/io/PrintStream.println:"(Ljava/lang/String;)V"; L33: stack_frame_type same; return; } } // end Class Foo
    展开
    共 1 条评论
    9
  • 迷上尼古丁的味道
    2019-02-22
    下面这段中0x8000000是不是少写了一个0呢 前者在 Java 里是 0,后者是符号位为 1、其他位均为 0 的浮点数,在内存中等同于十六进制整数 0x8000000(即 -0.0F 可通过 Float.intBitsToFloat(0x8000000) 求得)。
    共 1 条评论
    8
  • 杨春鹏
    2018-07-26
    局部变量中基本数据类型存储在栈中,变量的变量名(引用符号)和变量值(字面量)都存储在栈中。 局部变量中引用数据类型的引用地址存储在栈中,对象的实例数据存储在堆中,类型数据存储在方法区 全局变量的基本数据类型和引用数据类型,都存储在堆中。 不知理解的是否正确
    展开
    共 3 条评论
    8
  • 随心而至
    2019-09-25
    代码验证是可以的,3等奇数值最低位为1,就都打印;2等偶数值最低位为0,就都不打印,表明是掩码处理,取最低位。 public class Foo { private boolean flag; public boolean getFlag() { return flag; } public static void main(String[] args) throws NoSuchFieldException, IllegalAccessException { Unsafe unsafe = getUnsafe(); Foo foo = new Foo(); Field field = foo.getClass().getDeclaredField("flag"); long offset = unsafe.objectFieldOffset(field); unsafe.putInt(foo, offset, 3); if (foo.getFlag()) System.out.println("Hello,Java"); if (foo.getFlag() == true) System.out.println("Hello, JVM"); } public static Unsafe getUnsafe() throws NoSuchFieldException, IllegalAccessException { Field f = Unsafe.class.getDeclaredField("theUnsafe"); f.setAccessible(true); return (Unsafe) f.get(null); } }
    展开
    6
  • neohope
    2019-02-14
    测试了3个JDK版本,发现boolValue = true 里的 true 换为 2 或 3的例子,对JRE的版本是有一定要求的。同一个修改后的class文件(JDK8编译),在不同的环境中运行,输出分别为: windows+jdk1.8,2 输出第一个,3输出第一个; Linux+openjdk1.10,2 不输出,3两个都输出; windows+jdk1.11,2 不输出,3两个都输出; 建议还是用JDK10以上的版本。
    展开
    6
  • 志远
    2018-07-23
    NaN 有一个有趣的特性:除了“!= 始终返回 true”之外,所有其他比较结果都会返回 false。这句话好拗口啊,双引号的标点符号有问题吧

    作者回复: 多谢指出!应该是 “!=“

    6
  • Arvin
    2018-10-18
    当改为2或者3时则出现编译错误是则么回事!!!!
    5
  • 星星
    2018-09-25
    long,double,float三种类型存入操作数栈有做转化操作吗?还是做浮点运算会有特殊处理,本文没有提及呀。

    作者回复: 同样会加载至操作数栈上,但都当成各自类型来计算,而不是像其他几种,被当成int来计算

    5