性能文章>Integer的缓存模式,自动拆装箱,关于128!=128的问题>

Integer的缓存模式,自动拆装箱,关于128!=128的问题原创

4119411

 **本文首发于公众号【看点代码再上班】,建议关注公众号,及时阅读最新文章。**

一定要读的原文:https://mp.weixin.qq.com/s?__biz=MzIwM……

 

大家好,我是tin,这是我的第19篇原创文章

我们都遇到过Integer a=128,Integer b=128,但a==b不成立的困惑,今天结合源码和Java的拆装箱说一说其中的原由,先上一个目录:

一、值相等的Integer,==却不成立?

二、自动拆/装箱

2.1 自动装箱

2.2 自动拆箱

三、其他基本类型的缓存

四、结语


 

一、值相等的Integer,==却不成立?

有两个变量a和b,对应分别赋相同的值,但不同情况下的赋值,==号的比较得到的结果却是截然不同。

package com.tin.example.lang;
​
/**
 * title: IntegerTest
 * <p>
 * description: Integer包装类相关测试
 *
 * @author tin @看点代码再上班 on 2021/5/15 下午1:28
 */
public class IntegerTest {
​
    public static void main(String[] args) throws InterruptedException {
        Integer a = 127;
        Integer b = 127;
        System.out.println("res:" + (a == b));
        a = 128;
        b = 128;
        System.out.println("res:" + (a == b));
        a = -128;
        b = -128;
        System.out.println("res:" + (a == b));
        a = -129;
        b = -129;
        System.out.println("res:" + (a == b));
    }
}

@示例代码1

 

当我们运行以上示例代码,输出结果是这样的:

res:true
res:false
res:true
res:false

 

看到这,我们应该都会觉得奇怪,为什么有些情况下相等,有些情况就不相等了?

我们都知道,==比较的是对象地址引用,不相等说明对象所在的内存地址不一样。

试猜想:相等是因为指向同一个对象,不相等是因为指向不同的对象。

那么,为什么有些值指向同一个对象,有些值却没有指向同一个对象?

要知道真正原因,一看源码便知,源码底下无秘密。

 

首先,我们要看的是java.lang.Integer#valueOf(int)方法的源码,其实现如下:

/**
     * 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);
    }

 

API描述得很清楚了,这个方法始终缓存-128~127之间的值,且,还有可能缓存此范围以外的值。

我们来理解这句话后面的真正原理:

1、始终缓存-128~127之间的值

直接打开java.lang.Integer.IntegerCache的源码。

java.lang.Integer.IntegerCache是Integer的一个内部类,它唯一的作用就是实现本地缓存,其内部有一个static代码块:

其义就在于JVM启动的时候,把-128~127之间的整数放到cache内。

同时也可以看到,放到cache内的都是new Integer()得到的对象。

所以,valueOf方法中IntegerCache.cache[i + (-IntegerCache.low)]的写法也就是读取缓存了,缓存内相同int值对应的是同一个缓存对象。

以上示例中的a=127b=127都能从缓存读取,且读取到的是同一个对象,==比较的结果是true没问题。

这是由源码决定的,-128~127之间的整数已被缓存起来,超过这个范围的int值不在缓存内,通过new Integer(i)实例化,这时==比较的结果自然就是false。

2、还有可能缓存-128~127以外的值

我们可以自定义IntegerCache缓存值范围的上限,比如我可以把上限改为256,也就是支持缓存-128~256之间的整数。

在启动参数中增加以下代码:

-Djava.lang.Integer.IntegerCache.high=256

我们再次运行@示例代码1,得到结果是这样的:

很明显,Integer a = 128和Integer b=128是相等的,==对比的结果是true。

还可以用另一个VM参数设置,达到同样的效果,如下:

-XX:AutoBoxCacheMax=256

支持这样的设置在源码中也有对应的体现:

首先取自定义的值,如果没有自定义则默认127,但如果设置的值小于127,默认也是127,也即,上限最小支持127。

二、自动拆/装箱

什么是自动装箱和拆箱?

装箱就是Java自动将基本类型转换成对应的包装类型,比如将int的变量转换成Integer对象,这个过程叫做装箱,反之将包装类型转换成基本类型的过程就叫做拆箱,比如将Integer对象转换成int类型值。因为这里的装箱和拆箱是Java内部机制自动完成,所以就称作为自动装箱和拆箱

基本类型byte, short, char, int, long, float, double 和 boolean 对应的封装类为Byte, Short, Character, Integer, Long, Float, Double, Boolean,它们都有自动拆装箱的过程。

2.1 自动装箱

Integer a = 127语句没有使用到java.lang.Integer#valueOf(int),为什么我却去看这个方法的源码?

原因很简单,Integer a = 127并不是等同于new Integer(127),而是等同于Integer.valueOf(127),其中就涉及到Java的自动装箱原理。

关于Java是什么时候做装箱的问题,我们需要打开查看class的汇编指令代码。

执行以下命令:

javac IntegerTest.javajavap -c IntegerTest

得到以下编码:

Compiled from "IntegerTest.java"
public class com.tin.example.lang.IntegerTest {
  public com.tin.example.lang.IntegerTest();
    Code:
       0: aload_0
       1: invokespecial #1                  // Method java/lang/Object."<init>":()V
       4: return
​
  public static void main(java.lang.String[]) throws java.lang.InterruptedException;
    Code:
       0: bipush        127
       2: invokestatic  #2                  // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer;
       5: astore_1
       6: bipush        127
       8: invokestatic  #2                  // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer;
      11: astore_2
      12: getstatic     #3                  // Field java/lang/System.out:Ljava/io/PrintStream;
      15: aload_1
      16: aload_2
      17: if_acmpne     24
      20: iconst_1
      21: goto          25
      24: iconst_0
      25: invokedynamic #4,  0              // InvokeDynamic #0:makeConcatWithConstants:(Z)Ljava/lang/String;
      30: invokevirtual #5                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
      33: sipush        128
      36: invokestatic  #2                  // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer;
      39: astore_1
      40: sipush        128
      43: invokestatic  #2                  // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer;
      46: astore_2
      47: getstatic     #3                  // Field java/lang/System.out:Ljava/io/PrintStream;
      50: aload_1
      51: aload_2
      52: if_acmpne     59
      55: iconst_1
      56: goto          60
      59: iconst_0
      60: invokedynamic #4,  0              // InvokeDynamic #0:makeConcatWithConstants:(Z)Ljava/lang/String;
      65: invokevirtual #5                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
      68: bipush        -128
      70: invokestatic  #2                  // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer;
      73: astore_1
      74: bipush        -128
      76: invokestatic  #2                  // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer;
      79: astore_2
      80: getstatic     #3                  // Field java/lang/System.out:Ljava/io/PrintStream;
      83: aload_1
      84: aload_2
      85: if_acmpne     92
      88: iconst_1
      89: goto          93
      92: iconst_0
      93: invokedynamic #4,  0              // InvokeDynamic #0:makeConcatWithConstants:(Z)Ljava/lang/String;
      98: invokevirtual #5                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
     101: sipush        -129
     104: invokestatic  #2                  // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer;
     107: astore_1
     108: sipush        -129
     111: invokestatic  #2                  // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer;
     114: astore_2
     115: getstatic     #3                  // Field java/lang/System.out:Ljava/io/PrintStream;
     118: aload_1
     119: aload_2
     120: if_acmpne     127
     123: iconst_1
     124: goto          128
     127: iconst_0
     128: invokedynamic #4,  0              // InvokeDynamic #0:makeConcatWithConstants:(Z)Ljava/lang/String;
     133: invokevirtual #5                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
     136: return
}

两个关键命令语句解释如下:

  • bipush 127:表示当 int 取值 -128~127 时,JVM 把对应常量压入栈中。

  • invokestatic #2:invokestatic是方法调用的字节码指令,#2表示符号引用在常量池中的索引号,根据这个索引号检索常量表,可以查到最终表示的是一个字符串字面量,例如java/lang/Integer.valueOf:(I)Ljava/lang/Integer,这个就是方法的符号引用。

为了方便理解字节码,javap反编译的字节码都会给符号引用加注释,注释标明该符号引用的最终表示值,例如java/lang/Object."<init>":()V。

符号引用(Symbolic References)是一个用来无歧义地标识一个实体(例如方法/字段)的字符串,在运行期它会翻译为直接引用(Direct Reference)。对于方法来说,就是方法的入口地址。

所以,方法调用的本质是根据方法的符号引用确定方法的直接引用(入口地址)

在Java编译的时候就已经决定了Integer a = 127等同于Integer a = Integer.valueOf(127),所以,我们上文中才会分析Integer.valueOf方法的源码。

2.2 自动拆箱

自动拆箱,和装箱刚好相反。

我们写如下demo代码:

Integer b = 500;int c = 500;System.out.println("res:" + (b == c));

猜都可以猜到了, b == c是成立的。

同样的,我们通过反编译java文件,看看最终的汇编指令是如何的:

在执行到b == c时候,Java把Integer包装类型进行了拆箱,b等同于b.intValue()java.lang.Integer#intValue返回值就是基础类型值,源码如下:

所以,b == c成立。

三、其他基本类型的缓存

除了Integer,其他7种基本类型对应的包装类都有装箱实现,首先看一看各自的valueOf源码:

   //boolean原生类型自动装箱成Boolean
    public static Boolean valueOf(boolean b) {
        return (b ? TRUE : FALSE);
    }
​
    //byte原生类型自动装箱成Byte
    public static Byte valueOf(byte b) {
        final int offset = 128;
        return ByteCache.cache[(int)b + offset];
    }
​
    //byte原生类型自动装箱成Byte
    public static Short valueOf(short s) {
        final int offset = 128;
        int sAsInt = s;
        if (sAsInt >= -128 && sAsInt <= 127) { // must cache
            return ShortCache.cache[sAsInt + offset];
        }
        return new Short(s);
    }
​
    //char原生类型自动装箱成Character
    public static Character valueOf(char c) {
        if (c <= 127) { // must cache
            return CharacterCache.cache[(int)c];
        }
        return new Character(c);
    }
​
    //int原生类型自动装箱成Integer
    public static Integer valueOf(int i) {
        if (i >= IntegerCache.low && i <= IntegerCache.high)
            return IntegerCache.cache[i + (-IntegerCache.low)];
        return new Integer(i);
    }
​
    //int原生类型自动装箱成Long
    public static Long valueOf(long l) {
        final int offset = 128;
        if (l >= -128 && l <= 127) { // will cache
            return LongCache.cache[(int)l + offset];
        }
        return new Long(l);
    }
​
    //double原生类型自动装箱成Double
    public static Double valueOf(double d) {
        return new Double(d);
    }
​
    //float原生类型自动装箱成Float
    public static Float valueOf(float f) {
        return new Float(f);
    }

从valueOf方法就可以看得出来,除了Boolean、Double和Float没有缓存以外,其余的包装类型都有缓存实现。各个包装类型对应的缓存值范围如下:

基本类型 大小 包装类型 缓存范围 是否支持自定义缓存范围
boolean 6bit Bloolean / /
char 8bit Character 0~127
byte 8bit Byte -128~127
short 16bit Short -128~127
int 32bit Integer -128~127 支持
long 64bit Long -128~127
float 32bit Float / /
double 64bit Double / /

 

四、结语

我是tin,一个在努力让自己变得更优秀的普通工程师。自己阅历有限、学识浅薄,如有发现文章不妥之处,非常欢迎加我提出,我一定细心推敲并加以修改。

看到这里请安排个“三连”(分享、点赞、在看)再走吧,坚持创作不容易,不要白嫖,你的正反馈是我坚持输出的最强大动力,谢谢!

最后再附上原文链接:
https://mp.weixin.qq.com/s?__biz=MzIwM……

点赞收藏
看点代码再上班
请先登录,查看4条精彩评论吧
快去登录吧,你将获得
  • 浏览更多精彩评论
  • 和开发者讨论交流,共同进步

为你推荐

随机一门技术分享之Netty

随机一门技术分享之Netty

MappedByteBuffer VS FileChannel:从内核层面对比两者的性能差异

MappedByteBuffer VS FileChannel:从内核层面对比两者的性能差异

Netty源码解析:writeAndFlush

Netty源码解析:writeAndFlush

11
4