《阿里巴巴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 property
int 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_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
public static void main(java.lang.String[]);
Code:
0: bipush 100
2: invokestatic #2 // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer;
5: astore_1
6: bipush 100
8: invokestatic #2 // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer;
11: astore_2
12: sipush 150
15: invokestatic #2 // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer;
18: astore_3
19: sipush 150
22: invokestatic #2 // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer;
25: astore 4
27: getstatic #3 // Field java/lang/System.out:Ljava/io/PrintStream;
30: aload_1
31: aload_2
32: if_acmpne 39
35: iconst_1
36: goto 40
39: iconst_0
40: invokevirtual #4 // Method java/io/PrintStream.println:(Z)V
43: getstatic #3 // Field java/lang/System.out:Ljava/io/PrintStream;
46: aload_3
47: aload 4
49: if_acmpne 56
52: iconst_1
53: goto 57
56: iconst_0
57: invokevirtual #4 // Method java/io/PrintStream.println:(Z)V
60: 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
,程序结束。