美文网首页程序员在成长Java 杂谈
面试系列之Integer缓存所引发的惨案(保证看完你就彻底明白)

面试系列之Integer缓存所引发的惨案(保证看完你就彻底明白)

作者: 冬天只爱早晨 | 来源:发表于2019-08-28 14:27 被阅读0次

    今天在整理代码的时候发现了一段程序,如下

    Integer integer1 = 3;
    Integer integer2 = 3;
    
    if (integer1 == integer2)
      System.out.println("integer1 == integer2");
    else
      System.out.println("integer1 != integer2");
    
    Integer integer3 = 129;
    Integer integer4 = 129;
    
    if (integer3 == integer4)
      System.out.println("integer3 == integer4");
    else
      System.out.println("integer3 != integer4");
    
    System.out.println(Integer.valueOf("127")==Integer.valueOf("127"));
    System.out.println(Integer.valueOf("128")==Integer.valueOf("128"));
    System.out.println(Integer.parseInt("128")==Integer.valueOf("128"));
    

    我看了一眼,只记得答案是

    integer1 == integer2
    integer3 != integer4
    true
    false
    true
    

    我刚想把代码关闭,脑海里突然黑人问号?

    为什么会是这样?

    还别说,我竟然一时语噻,说不出个所以然来,考虑到这是一个面试题,而且也想去了解下设计原理,索性花了点时间去重新整理了一下,做个记录!

    说明:本文使用的Java版本号如下

    java version "1.8.0_121"
    Java(TM) SE Runtime Environment (build 1.8.0_121-b13)
    Java HotSpot(TM) 64-Bit Server VM (build 25.121-b13, mixed mode)
    

    缓存

    这里考察的主要是包装类型的缓存,我们点开Integer的源码,会发现如下代码

    private static class IntegerCache {
      // 最小值
      static final int low = -128;
      // 最大值,支持自定义
      static final int high;
      // 缓存数组
      static final Integer cache[];
    
      static {
        // 最大值可以通过属性配置来改变
        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);
            // 最大数组大小为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;
        // 将low-high范围内的值全部实例化并存入数组中当缓存使用
        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() {}
    }
    

    这里就是Integer包装类型里的缓存声明:

    Integer第一次使用的时候就会初始化缓存,其中范围最小值为-128,最大值默认是127,它可以通过-Djava.lang.Integer.IntegerCache.high=xxx或者-XX:AutoBoxCacheMax=xxx来设置,如下图:

    1.png 2.jpg

    接着会把low至high中所有的数据初始化存入数据中,默认就是将-128~127总共256个数循环实例化存入cache数组中。准确的说应该是将这256个对象在内存中的地址存进数组中。

    再来看看其他包装类型中缓存的支持;

    基本类型 大小 最小值 最大值 包装器类型 缓存范围 是否支持自定义
    boolean - - Bloolean
    char 6bit Unicode 0 Unic ode 2(16)-1 Character 0~127
    byte 8bit -128 +127 Byte -128~127
    short 16bit -2(15) 2(15)-1 Short -128~127
    int 32bit -2(31) 2(31)-1 Integer -128~127 支持
    long 64bit -2(63) 2(63)-1 Long -128~127
    float 32bit IEEE754 IEEE754 Float -
    double 64bit IEEE754 IEEE754 Double -
    void - - - Void

    这里又来了多个黑人问号

    • -128~127怎么来的?
    • 为什么是-128 ~ 127?怎么不是-200 ~ 200呢?
    • 为什么需要缓存数据?

    后面会讲到,我们先把程序走完~

    对象的初始化

    通过使用IDEA的Show ByteCode我们可以得到代码在JVM里的亚子,如图

    3.png

    我们可以看到Integer integer1 = 3; 实际上是通过Integer.valueOf返回一个Integer对象,我们再进入源码,它的最终现实如下:

    public static Integer valueOf(int i) {
      if (i >= IntegerCache.low && i <= IntegerCache.high)
        // int j = low;cache[k] = new Integer(j++);
        return IntegerCache.cache[i + (-IntegerCache.low)];
      return new Integer(i);
    }
    

    很明显了,首先判断i是否在已经被缓存,如果是的话直接从缓存中取出(地址)返回,否则就重新实例化一个。
    注意: 恶心的就在这里,有就从缓存拿,没有就实例化!这也就牵扯出Integer Swap的问题,后面文章会讲到。

    对象之间的比较

    ==

    • 基本数据类型:byte,short,char,int,long,double,float,blooean,它们之间的比较,比较是它们的值;
    • 引用数据类型:使用==比较的时候,比较的则是它们在内存中的地址(heap上的地址)。

    equals

    这个是Object下的一个方法,对应代码如下:

    public boolean equals(Object obj) {
      return (this == obj);
    }
    

    默认也是比较两个对象之间的内存地址,而其他对象在继承Object的时候一般会去Overwrite此方法,所以在没有Overwrite的情况下用equals的结果跟==一样都是比较内存地址,否则则按照Overwrite规则来。

    答案分析

    再来看看本处的代码,使用==则认为是比较前后两者的内存地址(以下比较均使用默认的缓存大小即-128~127)

    • 第一个比较integer1 == integer2前后都是3,通过上述的对象初始化我们可以知道integer1integer2均在缓存数组中,所以Integer直接从缓存数组中去取出地址返回,那边显而易见的,当使用==的时候则为true;
    • 第二个比较integer3 == integer4前后都是129,不在-128~127范围内,则Integer需要重新实例化,所以integer3integer4都是重新实例化的对象,对应的地址自然而然的也就不一样了,所以结果为false;
    • 第三和和第四个同理;
    • 第五个比较 Integer.parseInt("128")==Integer.valueOf("128")有意思,我们先看右边,128不在缓存内需要重新实例化一个,再看左边Integer.parseInt("128")返回值是int型,最终的比较变成了128==Integer(128),再看ByteCode,如下图,我们会发现Integer(128)最终使用的是Integer.intValue ()方法,哈哈,竟做了拆箱处理,finally,比较就变成了128==128,结果当然就是true了。
    4.png

    扩展

    回到第二节的两个问题:

    • -128~127怎么来的?
    • 为什么是-128 ~ 127?怎么不是-200 ~ 200呢?
    • 为什么需要缓存数据?

    后来在网上查找,找到一个比较靠谱的解释

    实际上,在Java 5中首次引入此功能时,范围固定为-127到+127。 后来在Java 6中,范围的最大值映射到java.lang.Integer.IntegerCache.high,VM参数允许我们设置高位数。 根据我们的应用用例,它可以灵活地调整性能。 应该从-127到127选择这个数字范围的原因应该是什么。这被认为是广泛使用的整数范围。 在程序中首次使用Integer必须花费额外的时间来缓存实例。

    Java Language Specification 的说明如下:

    Ideally, boxing a given primitive value p, would always yield an identical reference. In practice, this may not be feasible using existing implementation techniques. The rules above are a pragmatic compromise. The final clause above requires that certain common values always be boxed into indistinguishable objects. The implementation may cache these, lazily or eagerly. For other values, this formulation disallows any assumptions about the identity of the boxed values on the programmer's part. This would allow (but not require) sharing of some or all of these references.

    This ensures that in most common cases, the behavior will be the desired one, without imposing an undue performance penalty, especially on small devices. Less memory-limited implementations might, for example, cache all char and short values, as well as int and long values in the range of -32K to +32K.

    API 上解释

    Returns an Integer instance representing the specified int value. If a new Integer instance is not required, this method should generally be used in preference to the constructor 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

    我的理解就是:

    • -128~127范围内的数是比较频繁使用的,JDK就增加了这一默认的范围但并不是不可变的,毕竟JDK提供了两种方法供用户自定义范围;
    • 事先做缓存还是为了提高空间和时间性能!
      (大家参照上面官方与非官方的解释,自己意会哈~)

    附加题

    下面的输出是什么:

    Integer integer5 = new Integer(3);
    Integer integer6 = new Integer(3);
    if (integer5 == integer6)
      System.out.println("integer5 == integer6");
    else
      System.out.println("integer5 != integer6");
    

    本文的示例代码: Github

    参考:

    https://stackoverflow.com/questions/20897020/why-integer-class-caching-values-in-the-range-128-to-127

    https://stackoverflow.com/questions/20877086/why-do-comparisons-with-integer-valueofstring-give-different-results-for-12

    https://docs.oracle.com/javase/specs/jls/se7/html/jls-5.html#jls-5.1.7

    https://javapapers.com/java/java-integer-cache/

    相关文章

      网友评论

        本文标题:面试系列之Integer缓存所引发的惨案(保证看完你就彻底明白)

        本文链接:https://www.haomeiwen.com/subject/gcnpectx.html