说明:对于 Integer var = ? 在 - 128 至 127 范围内的赋值,Integer 对象是在 IntegerCache.cache 产 生,会复用已有对象,这个区间内的 Integer 值可以直接使用 == 进行判断,但是这个区间之外的所有数据,都会在堆上产生,并不会复用已有对象,这是一个大坑,推荐使用 equals 方法进行判断。
为什么这么说呢?
先看看这个题:Integer var = ? 会缓存 -128 到 127 之间的赋值?
public class Test {
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);
System.out.println(a.equals(b));
System.out.println(c.equals(d));
}
}
以上的答案是多少呢?你答对了么?
true
false
true
true
那么经过代码规约之后呢?看似正确的代码会提示错误么?
我们反编译之后呢?
Compiled from "Test.java"
public class test.Test {
public test.Test();
Code:
0: aload_0
1: invokespecial #1 // Method java/lang/Object."":()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
}
编译源代码: javac Test.java
对编译后的类文件进行反汇编: javap -c Test.class
得到下面反编译的代码:
偏移为 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
,程序结束。
上面的指令看懂了么?我们从上述代码中发现 Ineger.valueOf(int)
的确是通过 java.lang.Ineger#valueOf(int)
来构造对象的。
我们再聊聊缓存问题?
public static Integer valueOf(int i) {
if (i >= IntegerCache.low && i <= IntegerCache.high)
return IntegerCache.cache[i + (-IntegerCache.low)];
return new Integer(i);
}
那么Ineger
Cache里面到底是怎么缓存的呢?
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");
// 省略其它代码
}
// 省略其它代码
}
这本质上属于空间换时间,预存这部分数据就要占空间,但是下次用的时候就不需要重新创建。 另外体现了对象池设计模式,缓存这部分数据直接复用又避免了重复重复创建。
敲重点:如果想减少内存占用,提高程序运行的效率,可以将常用的对象提前缓存起来,需要时直接从缓存中提取。
另附上JVM指令手册:链接:https://pan.baidu.com/s/10_Pm613_fIlDnUIzGO43ZQ 提取码:h4xn
剖析《阿里巴巴 Java 开发手册》:https://www.imooc.com/read/55