Integer的-128~127值缓存问题的思考

2,757 阅读7分钟

引言

闲来无事,随意点了项目中代码的一些源码,想看看这些代码中依赖的源码中都怎么实现的,机缘巧合进入Integer的源码。复习之后记录一下自己的心得。

闲话少说,代码敬上

private static void demo4(){
    int a = 128, b = 128;
    System.out.println("run result NO.1->"+(a == b));
  
    Integer c = 128, d = 128;
    System.out.println("run result NO.2->"+(c == d));
   
    Integer e = 100, f = 100;
    System.out.println("run result NO.3->"+(e == f));
}

代码执行结果:

run result NO.1->true
run result NO.2->false
run result NO.3->true

从执行上可以看出,变量c、d都是128而执行结果是false。这里就涉及到自动拆装箱机制。

分析

自动拆装箱

百度百科中关于自动拆装箱的解释:

Java拆装箱就是Java相应的基本数据类型和引用类型的互相转化

Integer a = 1; 其中 aInteger类型,而1int类型,且Integerint之间并没有继承关系,按照java一般情况处理运行程序会报异常。但是因为自动拆装箱的存在,在为Integer类型的变量赋int类型的值时,Java会自动将int类型的转换为Integer类型。 即 Integer a = Integer.valueOf(1);

valueOf()在Java api中解释为:

//返回一个 Integer指定的 int值的 Integer实例。 
static Integer valueOf(int i) 
//返回一个 Integer对象,保存指定的值为 String 。  
static Integer valueOf(String s) 
//返回一个 Integer对象,保存从指定的String中 String的值,当用第二个参数给出的基数进行解析时。
static Integer valueOf(String s, int radix) 

为什么执行==后结果不相同?

看了一些概念后我我们回归问题的本身。我们先增加一下辅助信息来分析问题:

 private static void demo4(){
        int a = 128, b = 128;
        System.out.println("run result NO.1->"+(a == b));
        System.out.println("a->identityHashCode="+System.identityHashCode(a));
        System.out.println("b->identityHashCode="+System.identityHashCode(b));

        Integer c = 128, d = 128;
        System.out.println("run result NO.2->"+(c == d));
        System.out.println("c->identityHashCode="+System.identityHashCode(c));
        System.out.println("d->identityHashCode="+System.identityHashCode(d));

        Integer e = 100, f = 100;
        System.out.println("run result NO.3->"+(e == f));
        System.out.println("e->identityHashCode="+System.identityHashCode(e));
        System.out.println("f->identityHashCode="+System.identityHashCode(f));
    }

通过调用System.identityHashCode()方法打印变量的hash值进行分析问题:

run result NO.1->true
a->identityHashCode=1706377736
b->identityHashCode=468121027

run result NO.1->false
c->identityHashCode=1804094807
d->identityHashCode=951007336

run result NO.1->true
e->identityHashCode=2001049719
f->identityHashCode=2001049719

看执行结果发现,NO.1的结果true而变量a、b的hash值却不是相同。最开始的问题还没有找到原因怎么又出现了新的问题呢,先不要着急且往下看。

看看源码的实现

既然涉及到自动拆装箱机制,那么我们看看Integer.valueOf()的源码是怎么实现的

/**
     * Returns an {@code Integer} instance representing the specified
     * {@code int} value.  If a new {@code Integer} instance is not
     * required, this method should generally be used in preference to
     * the constructor {@link #Integer(int)}, as this method is likely
     * to yield significantly better space and time performance by
     * caching frequently requested values.
     *
     * This method will always cache values in the range -128 to 127,
     * inclusive, and may cache other values outside of this range.
     *
     * @param  i an {@code int} value.
     * @return an {@code Integer} instance representing {@code i}.
     * @since  1.5
     */
    public static Integer valueOf(int i) {
        if (i >= IntegerCache.low && i <= IntegerCache.high)
            return IntegerCache.cache[i + (-IntegerCache.low)];
        return new Integer(i);
    }

注释中说到参数i如果在-128~127之间,那么返回值就是从缓存中取出并返回的,反之就通过new Integer(i)的方式返回。源码中的IntegerCache是这样写的:

 /**
     * 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");
            if (integerCacheHighPropValue != null) {
                try {
                    int i = parseInt(integerCacheHighPropValue);
                    i = Math.max(i, 127);
                    // Maximum array size is Integer.MAX_VALUE
                    h = Math.min(i, Integer.MAX_VALUE - (-low) -1);
                } catch( NumberFormatException nfe) {
                    // If the property cannot be parsed into an int, ignore it.
                }
            }
            high = h;

            cache = new Integer[(high - low) + 1];
            int j = low;
            for(int k = 0; k < cache.length; k++)
                cache[k] = new Integer(j++);

            // range [-128, 127] must be interned (JLS7 5.1.7)
            assert IntegerCache.high >= 127;
        }
        private IntegerCache() {}
    }

通过源码和注释传递的信息我们清晰的得到,在其static块初始化时就一次性生成了-128到127直接的Integer类型变量存储在cache[]中,对于-128到127之间的int类型,返回的都是同一个Integer类型对象。

这下真相大白了,整个工作过程就是:Integer.class在装载(Java虚拟机启动)时,其内部类型IntegerCache的static块即开始执行,实例化并暂存数值在-128到127之间的Integer类型对象。当自动装箱int型值在-128到127之间时,即直接返回IntegerCache中暂存的Integer类型对象。

为什么这样设计呢?其实Integer.valueOf()的注释方法中就说明了这个原因:缓存频繁请求的值可以产生更多的空间和时间性能(请原谅我翻译的不够专业)

As this method is likely to yield significantly better space and time performance by caching frequently requested values.

假设不缓存,每当要自动拆装箱的时候都需要触发new,在堆中分配内存,就显得太慢了。如果是在死循环等极端情况会有FullGC的潜在风险,影响系统的性能。所以不如预先将那些常用的值提前生成好,自动装箱时直接拿出来返回。哪些值是常用的?就是-128到127了。

为什么缓存范围是-128~127?

说到这里就又有新的问题了,缓存的范围为什么是-128~127? 由于计算机只能识别二进制,即0和1。所以规定第一位是符号位,1表示负数,0表示正数。

正数:原码=反码=补码 负数:反码=原码的所有位(符号位除外)取反 补码=反码+1

而一个字节有8位,第1位是符号位,1代表负数,0代表正数。 所以一个字节: 最小正数二进制是0000 0000=0 最大正数二进制是0111 1111 = 64+32+16+8+4+2+1=127 最大负数二进制是1111 1111 = -1 最小负数二进制是1000 0000→ 反码:1111 1111→ 补码: -{(1+2+4+8+16+32+64)+1} =-(127+1)=-128 这就是为什么缓存范围是-128~127的原因了。

回归问题

说了这么多原理概念,再来说说文章一开始的问题。

  Integer c = 128, d = 128;
  System.out.println("run result NO.2->"+(c == d));

当执行c == d;时是先执行自动拆装箱在用==进行比较的,是通过new Integer(c),new Integer(d)返回结果的所以他们的内存地址是不相等,所以可以使用equals()来解决用==判断不相等问题。

 int a = 128, b = 128;
 System.out.println("run result NO.1->"+(a == b));

基本类型的==是比较两者之间的值是否相等,int类型定义的变量值在-128~127之间时是直接在缓存中取的,当超出这个区间时就要在栈中重新开辟一个区间存放,这就是为什么a与b相等而内存地址不相等的原因

延展

不仅int,Java中的另外7中基本类型都可以自动装箱和自动拆箱,其中也有用到缓存。见下表:

基本类型 装箱类型 取值范围 是否缓存 缓存范围
byte Byte -128 ~ 127 -128 ~ 127
short Short -2^15 ~ (2^15 - 1) -128 ~ 127
int Integer -2^31 ~ (2^31 - 1) -128 ~ 127
long Long -2^63 ~ (2^63 - 1) -128 ~ 127
float Float -- --
double Double -- --
boolean Boolean true, false true, false
char Character \u0000 ~ \uffff \u0000 ~ \u007f