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

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

196414

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

一定要读的原文: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……

请先登录,再评论

👍👍👍

2月前

为你推荐

字符串字面量长度是有限制的
前言 偶然在一次单元测试中写了一个非常长的字符串字面量。 正文 在一次单元测试中,我写了一个很长的字符串字面量,大概10万个字符左右,编译时,编译器给出了异常告警 `java: constant
多次字符串相加一定要用StringBuilder而不用-吗?
今天在写一个读取Java class File并进行分析的Demo时,偶然发现了下面这个场景(基于oracle jdk 1.8.0_144): ``` package test; public c
如何通过反射获得方法的真实参数名(以及扩展研究)
前段时间,在做一个小的工程时,遇到了需要通过反射获得方法真实参数名的场景,在这里我遇到了一些小小的问题,后来在部门老大的指导下,我解决了这个问题。通过解决这个问题,附带着我了解到了很多新的知识,我觉得
高吞吐、低延迟 Java 应用的 GC 优化实践
本篇原文作者是 LinkedIn 的 Swapnil Ghike,这篇文章讲述了 LinkedIn 的 Feed 产品的 GC 优化过程,虽然文章写作于 April 8, 2014,但其中的很多内容和
「每日五分钟,玩转 JVM」:久识你名,初居我心
聊聊 JVMJVM,一个熟悉又陌生的名词,从认识Java的第一天起,我们就会听到这个名字,在参加工作的前一两年,面试的时候还会经常被问到JDK,JRE,JVM这三者的区别。JVM可以说和我们是老朋友了
据说99.99%的人都会答错的类加载的问题
概述首先还是把问题抛给大家,这个问题也是我厂同学在做一个性能分析产品的时候碰到的一个问题。 同一个类加载器对象是否可以加载同一个类文件多次并且得到多个Class对象而都可以被java层使用吗请仔细注意
Java多线程——并发测试
编写并发程序时候,可以采取和串行程序相同的编程方式。唯一的难点在于,并发程序存在不确定性,这种不确定性会令程序出错的地方远比串行程序多,出现的方式也没有固定规则。那么如何在测试中,尽可能的暴露出这些问
Java多线程知识小抄集(一)
本文主要整理笔者遇到的Java多线程的相关知识点,适合速记,故命名为“小抄集”。本文没有特别重点,每一项针对一个多线程知识做一个概要性总结,也有一些会带一点例子,习题方便理解和记忆。 1.interr