读郑雨迪《深入拆解Java虚拟机》 -- 第二讲 Java的基本类型

本文转自https://time.geekbang.org/column/article/11503

 

Java不仅是一门面向对象的语言,它还引进8种基本数据类型来支持数值运算。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语句:

  1. 第一个if语句直接判断“吃过饭没”,在它成立的情况下,代码会打印“吃了”
  2. 第二个if语句判断“吃过饭没”和true是否相等,在它成立的情况下,代码会答应“真吃了

Java语言中的boolean类型

在Java语言规范中,boolean类型的值只有两种可能,它们分别用truefalse来表示,显然这两种符号是不能被虚拟机直接使用的、

在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等。

对于Java虚拟机来说,它所看到的boolean类型早就被映射为整数类型。因此,将原本声明为boolean类型的局部变量,赋值成为除了0、1之外的变量,在Java虚拟机看来是合法的。

Java的基本数据类型

除了上面提到的boolean类型外,Java的基本数据类型还包括byte、short、char、int、long、float、double

《读郑雨迪《深入拆解Java虚拟机》 -- 第二讲 Java的基本类型》

Java的基本数据类型都有其值域和默认值。可以看到,byte、short、char、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为例,浮点类型通常有2个0,+0.0F以及-0.0F。

前者在Java里是0,后者是符号位为1、其它位为0的浮点数,在内存中等同于十六进制整数0x80000000(即-0.0F 可通过 Float,intBitsToFloat(0x80000000)求得)。尽管它们的内存数值不同,但是在Java中+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来填充。

我们来完成老师布置的作业:将boolean类型的值存入字段中时,Java虚拟机所做的掩码操作。

首先,我们撰写Java代码:

public class Foo{
	static boolean boolValue;//这里不在栈区

	public static void main(String[] args){
		boolValue = true;
		if(boolValue)
			System.out.println("Hello Java!");
		if(boolValue == true)
			System.out.println("Hello JVM!");
	}
}

使用javac编译并用java运行它

javac Foo.java
java Foo
Hello Java!
Hello JVM!

使用asmtools.jar来反汇编class文件

java -cp ./asmtools.jar org.openjdk.asmtools.jdis.Main Foo.class > Foo.jasm.1

得到Foo.jasm.1文件


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

再运行指令(其作用为将Foo.jasm.1文件中第一个iconst_1 替换为iconst_2, 输出到文件Foo.jasm中)

awk 'NR==1,/iconst_1/{sub(/iconst_1/, "iconst_2")} 1' Foo.jasm.1 > Foo.jasm

得到Foo.jasm文件


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

现在我们将赋给boolValue的值为2,再将其汇编如Foo.class

java -cp ./asmtools.jar org.openjdk.asmtools.jasm.Main Foo.jasm

这时,我们再运行

java Foo

发现没有任何输出。

我们再按照上述步骤重复一遍,只需修改指令(即修改赋给boolValue的值为3)

 awk 'NR==1,/iconst_1/{sub(/iconst_1/, "iconst_3")} 1' Foo.jasm.1 > Foo.jasm

得到新的反汇编代码


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_3; //看这里
		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

再反汇编运行

java -cp ./asmtools.jar org.openjdk.asmtools.jasm.Main Foo.jasm
java Foo
Hello Java!
Hello JVM!

发现全部运行了。

结论:static修饰的成员不是存在栈区,所以在进行boolean类型赋值时会进行掩码操作,即只保留最低位,2的最低位为0,3的最低位为1,所以将3赋值给静态boolean类型变量时和true(1)没有任何差别。

此文从极客时间专栏《深入理解Java虚拟机》搬运而来,撰写此文的目的:

  1. 对自己的学习总结归纳

  2. 此篇文章对想深入理解Java虚拟机的人来说是非常不错的文章,希望大家支持一下郑老师。

    原文作者:java虚拟机
    原文地址: https://blog.csdn.net/Ti_an_Di/article/details/81318221
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞