《阿里巴巴Java开发手册》有一段关于包装对象之间值的比较问题的规约 :
【强制】所有整型包装类对象之间值的比较,全部使用 equals 方法比较。 说明:对于 Integer var = ? 在 - 128 至 127 范围内的赋值,Integer 对象是在 IntegerCache.cache 产 生,会复用已有对象,这个区间内的 Integer 值可以直接使用 == 进行判断,但是这个区间之外的所有数据,都会在堆上产生,并不会复用已有对象,这是一个大坑,推荐使用 equals 方法进行判断。
1. Integer 缓存问题分析
我们先看下面的示例代码,并思考该段代码的输出结果:
public class IntTest {public static void main(String[] args) {Integer a = 100, b = 100, c = 150, d = 150;System.out.println(a == b);System.out.println(c == d);}}
通过运行代码可以得到答案,程序输出的结果分别为: true , false。 因为缓存了 -128 到 127 之间的数值
2. 源码分析
我们知道,Integer var = ? 形式声明变量,会通过 java.lang.Integer#valueOf(int) 来构造 Integer 对象。
static final int low = -128;static final int high; //默认是127 但是可以通过配置修改public static Integer valueOf(int i) {if (i >= IntegerCache.low && i <= IntegerCache.high)return IntegerCache.cache[i + (-IntegerCache.low)];return new Integer(i);}
通过源码可以看出,如果用 Ineger.valueOf(int) 来创建整数对象,参数大于等于整数缓存的最小值(IntegerCache.low )并小于等于整数缓存的最大值( IntegerCache.high), 会直接从缓存数组 (java.lang.Integer.IntegerCache#cache) 中提取整数对象;否则会 new 一个整数对象。
2.1 缓存的区间的最大值和最小值是多少?
2.2那么为什么会缓存这一段区间的整数对象呢?
通过注释我们可以得知:如果不要求必须新建一个整型对象,缓存最常用的值(提前构造缓存范围内的整型对象),会更省空间,速度也更快。
这给我们一个非常重要的启发:
如果想减少内存占用,提高程序运行的效率,可以将常用的对象提前缓存起来,需要时直接从缓存中提取。
2.3 Integer 缓存的区间可以修改吗?
java.lang.Integer.IntegerCache 是Integer的一个内部类
/*** Cache to support the object identity semantics of autoboxing for values between* -128 and 127 (inclusive) as required by JLS.** The cache is initialized on first usage. The size of the cache* may be controlled by the {@code -XX:AutoBoxCacheMax=<size>} option.* During VM initialization, java.lang.Integer.IntegerCache.high property* may be set and saved in the private system properties in the* sun.misc.VM class.*/private static class IntegerCache {static final int low = -128;static final int high;static final Integer cache[];static {// high value may be configured by propertyint h = 127;String integerCacheHighPropValue =sun.misc.VM.getSavedProperty("java.lang.Integer.IntegerCache.high");// 省略其它代码}// 省略其它代码}
通过 IntegerCache 代码和注释我们可以看到,最小值是固定值 -128, 最大值并不是固定值,缓存的最大值是可以通过虚拟机参数 -XX:AutoBoxCacheMax=<size>} 或 -Djava.lang.Integer.IntegerCache.high=<value> 来设置的,未指定则为 127。因此可以通过修改这两个参数其中之一,让缓存的最大值大于等于 150。
如果作出这种修改,示例的输出结果便会是: true,true。
这段注释也解答了为什么要缓存这个范围的数据:
是为了自动装箱时可以复用这些对象。
我们可以参考 JLS(Java Language Specification Java语言规范) 的 Boxing Conversion 部分的相关描述。
在 -128 到 127 (含)之间的 int 类型的值,或者 boolean 类型的 true 或 false, 以及范围在’\u0000’和’\u007f’ (含)之间的 char 类型的数值 p, 自动包装成 a 和 b 两个对象时, 可以使用 a == b 判断 a 和 b 的值是否相等。
3. 反汇编法
首先编译源代码:javac IntTest.java
然后需要对代码进行反汇编,执行:javap -c IntTest
反编译后,我们得到以下代码:
Compiled from "IntTest.java"public class com.chujianyun.common.int_test.IntTest {public com.chujianyun.common.int_test.IntTest();Code:0: aload_01: invokespecial #1 // Method java/lang/Object."<init>":()V4: returnpublic static void main(java.lang.String[]);Code:0: bipush 1002: invokestatic #2 // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer;5: astore_16: bipush 1008: invokestatic #2 // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer;11: astore_212: sipush 15015: invokestatic #2 // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer;18: astore_319: sipush 15022: invokestatic #2 // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer;25: astore 427: getstatic #3 // Field java/lang/System.out:Ljava/io/PrintStream;30: aload_131: aload_232: if_acmpne 3935: iconst_136: goto 4039: iconst_040: invokevirtual #4 // Method java/io/PrintStream.println:(Z)V43: getstatic #3 // Field java/lang/System.out:Ljava/io/PrintStream;46: aload_347: aload 449: if_acmpne 5652: iconst_153: goto 5756: iconst_057: invokevirtual #4 // Method java/io/PrintStream.println:(Z)V60: return}
可以明确得 “看到” 这四个 `Integer var = ?形式声明的变量的确是通过java.lang.Integer#valueOf(int)来构造Integer 对象的。
接下来对汇编后的代码进行详细分析,如果看不懂可略过:
根据《Java Virtual Machine Specification : Java SE 8 Edition》,后缩写为 JVMS , 第 6 章 虚拟机指令集的相关描述以及《深入理解 Java 虚拟机》 414-149 页的 附录 B “虚拟机字节码指令表”。 我们对上述指令进行解读:
偏移为 0 的指令为:bipush 100 ,其含义是将单字节整型常量 100 推入操作数栈的栈顶;
偏移为 2 的指令为:invokestatic #2 // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer; 表示调用一个 static 函数,即 java.lang.Integer#valueOf(int);
偏移为 5 的指令为:astore_1 ,其含义是从操作数栈中弹出对象引用,然后将其存到第 1 个局部变量 Slot 中;
偏移 6 到 25 的指令和上面类似;
偏移为 30 的指令为 aload_1 ,其含义是从第 1 个局部变量 Slot 取出对象引用(即 a),并将其压入栈;
偏移为 31 的指令为 aload_2 ,其含义是从第 2 个局部变量 Slot 取出对象引用(即 b),并将其压入栈;
偏移为 32 的指令为 if_acmpn,该指令为条件跳转指令,if_ 后以 a 开头表示对象的引用比较。
由于该指令有以下特性:
if_acmpeq 比较栈两个引用类型数值,相等则跳转 if_acmpne 比较栈两个引用类型数值,不相等则跳转
由于 Integer 的缓存问题,所以 a 和 b 引用指向同一个地址,因此此条件不成立(成立则跳转到偏移为 39 的指令处),执行偏移为 35 的指令。
偏移为 35 的指令: iconst_1,其含义为将常量 1 压栈( Java 虚拟机中 boolean 类型的运算类型为 int ,其中 true 用 1 表示,详见 2.11.1 数据类型和 Java 虚拟机。
然后执行偏移为 36 的 goto 指令,跳转到偏移为 40 的指令。
偏移为 40 的指令:invokevirtual #4 // Method java/io/PrintStream.println:(Z)V。
可知参数描述符为 Z ,返回值描述符为 V。
根据 4.3.2 字段描述符 ,可知 FieldType 的字符为 Z 表示 boolean 类型, 值为 true 或 false。
根据 4.3.3 字段描述符 ,可知返回值为 void。
因此可以知,最终调用了 java.io.PrintStream#println(boolean) 函数打印栈顶常量即 true。
然后比较执行偏移 43 到 57 之间的指令,比较 c 和 d, 打印 false 。
执行偏移为 60 的指令,即 retrun ,程序结束。
