01.Integer緩存問題分析

1. 前言

《手冊》第 7 頁有一段關於包裝對象之間值的比較問題的規約html

【強制】全部整型包裝類對象之間值的比較,所有使用 equals 方法比較。
說明:對於 Integer var = ? 在 - 128 至 127 範圍內的賦值,Integer 對象是在 IntegerCache.cache 產 生,會複用已有對象,這個區間內的 Integer 值能夠直接使用 == 進行判斷,可是這個區間以外的全部數據,都會在堆上產生,並不會複用已有對象,這是一個大坑,推薦使用 equals 方法進行判斷。

這條建議很是值得你們關注, 並且該問題在 Java 面試中十分常見。java

咱們還須要思考如下幾個問題:git

  • 若是不看《手冊》,咱們如何知道Integer var = ?會緩存 -128 到 127 之間的賦值?
  • 爲何會緩存這個範圍的賦值?
  • 咱們如何學習和分析相似的問題?

2.Integer 緩存問題分析

咱們先看下面的示例代碼,並思考該段代碼的輸出結果:程序員

public class IntTest {
    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);
    }
}

經過運行代碼能夠獲得答案,程序輸出的結果分別爲:true,falsegithub

那麼爲何答案是這樣?面試

結合《手冊》的描述不少人可能會很有自信地回答:由於緩存了 -128 到 127 之間的數值,就沒有而後了。數組

那麼爲何會緩存這一段區間的數值?緩存的區間能夠修改嗎?其它的包裝類型有沒有相似緩存?緩存

what? 咋還有這麼多問題?這誰知道啊oracle

莫急,且看下面的分析。jvm

2.1 源碼分析法

首先咱們能夠經過源碼對該問題進行分析。

咱們知道,Integer var = ?形式聲明變量,會經過java.lang.Integer#valueOf(int)來構造Integer對象。

不少人可能會說:「你咋能知道這個呢」?

若是不信你們能夠打斷點,運行程序後會調到這裏,總該信了吧?(後面還會再做解釋)。

咱們先看該函數源碼:

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

經過源碼能夠看出,若是用Ineger.valueOf(int)來建立整數對象,參數大於等於整數緩存的最小值(IntegerCache.low)並小於等於整數緩存的最大值(IntegerCache.high), 會直接從緩存數組 (java.lang.Integer.IntegerCache#cache) 中提取整數對象;不然會new一個整數對象。

那麼這裏的緩存最大和最小值分別是多少呢?

從上述註釋中咱們能夠看出,最小值是 -128, 最大值是 127。

那麼爲何會緩存這一段區間的整數對象呢?

經過註釋咱們能夠得知:若是不要求必須新建一個整型對象,緩存最經常使用的值(提早構造緩存範圍內的整型對象),會更省空間,速度也更快。

這給咱們一個很是重要的啓發:

若是想減小內存佔用,提升程序運行的效率,能夠將經常使用的對象提早緩存起來,須要時直接從緩存中提取。

那麼咱們再思考下一個問題:Integer緩存的區間能夠修改嗎?

經過上述源碼和註釋咱們還沒法回答這個問題,接下來,咱們繼續看java.lang.Integer.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");
           // 省略其它代碼
    }
      // 省略其它代碼
}

經過IntegerCache代碼和註釋咱們能夠看到,最小值是固定值 -128, 最大值並非固定值,緩存的最大值是能夠經過虛擬機參數-XX:AutoBoxCacheMax=<size>}-Djava.lang.Integer.IntegerCache.high=<value>來設置的,未指定則爲 127。

所以能夠經過修改這兩個參數其中之一,讓緩存的最大值大於等於 150。

若是做出這種修改,示例的輸出結果便會是:true,true

學到這裏是否是發現,對此問題的理解和最初的想法有些不一樣呢?

這段註釋也解答了爲何要緩存這個範圍的數據:

是爲了自動裝箱時能夠複用這些對象,這也是 JLS的要求。

咱們能夠參考 JLS 的Boxing Conversion 部分的相關描述。

If the value pbeing boxed is an integer literal of type intbetween -128and 127inclusive ( §3.10.1), or the boolean literal trueor false( §3.10.3), or a character literal between '\u0000'and '\u007f'inclusive ( §3.10.4), then let aand bbe the results of any two boxing conversions of p. It is always the case that a== b.

在 -128 到 127 (含)之間的 int 類型的值,或者 boolean 類型的 true 或 false, 以及範圍在’\u0000’和’\u007f’ (含)之間的 char 類型的數值 p, 自動包裝成 a 和 b 兩個對象時, 可使用 a == b 判斷 a 和 b 的值是否相等。

2.2 反彙編法

那麼究竟Integer var = ?形式聲明變量,是否是經過java.lang.Integer#valueOf(int)來構造Integer對象呢? 總不能都是猜想 N 個可能的函數,而後斷點調試吧?

若是遇到其它相似的問題,沒人告訴我底層調用了哪一個方法,該怎麼辦?囧…

這類問題有個殺手鐗,能夠經過對編譯後的 class 文件進行反彙編來查看。

首先編譯源代碼:javac IntTest.java

而後須要對代碼進行反彙編,執行:javap -c IntTest

若是想了解 javap的用法,直接輸入 javap -help查看用法提示(不少命令行工具都支持 -help--help給出用法提示)。
image.png

反編譯後,咱們獲得如下代碼:

Compiled from "IntTest.java"
public class com.chujianyun.common.int_test.IntTest {
  public com.chujianyun.common.int_test.IntTest();
    Code:
       0: aload_0
       1: invokespecial #1                  // Method java/lang/Object."<init>":()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
}

能夠明確得 "看到" 這四個 ``Integer var = ?形式聲明的變量的確是經過java.lang.Integer#valueOf(int)來構造Integer` 對象的。


接下來對彙編後的代碼進行詳細分析,若是看不懂可略過

根據《Java Virtual Machine Specification : Java SE 8 Edition》,後縮寫爲 JVMS , 第 6 章虛擬機指令集的相關描述以及《深刻理解 Java 虛擬機》414-149 頁的 附錄 B 「虛擬機字節碼指令表」。 咱們對上述指令進行解讀:

偏移爲 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類型, 值爲truefalse
根據4.3.3 字段描述符,可知返回值爲void

所以能夠知,最終調用了java.io.PrintStream#println(boolean)函數打印棧頂常量即true

而後比較執行偏移 43 到 57 之間的指令,比較 c 和 d, 打印false

執行偏移爲 60 的指令,即retrun,程序結束。


可能有些朋友會對反彙編的代碼有些抵觸和恐懼,這都是很是正常的現象。

咱們分析和研究問題的時候,看懂核心邏輯便可,不要糾結於細節,而失去了重點。

一回生兩回熟,隨着遇到的例子愈來愈多,遇到相似的問題時,會喜歡上javap來分析和解決問題。

若是想深刻學習 java 反彙編,強烈建議結合官方的 JVMS 或其中文版:《Java 虛擬機規範》這本書進行拓展學習。


若是你們不喜歡命令行的方式進行 Java 的反彙編,這裏推薦一個簡單易用的可視化工具:classpy,你們能夠自行了解學習。
image.png

3.Long 的緩存問題分析

咱們學習的目的之一就是要學會觸類旁通。所以咱們對Long也進行相似的研究,探究二者之間有何異同。

3.1 源碼分析

相似的,咱們接下來分析java.lang.Long#valueOf(long)的源碼:

/**
 * Returns a {@code Long} instance representing the specified
 * {@code long} value.
 * If a new {@code Long} instance is not required, this method
 * should generally be used in preference to the constructor
 * {@link #Long(long)}, as this method is likely to yield
 * significantly better space and time performance by caching
 * frequently requested values.
 *
 * Note that unlike the {@linkplain Integer#valueOf(int)
 * corresponding method} in the {@code Integer} class, this method
 * is <em>not</em> required to cache values within a particular
 * range.
 *
 * @param  l a long value.
 * @return a {@code Long} instance representing {@code l}.
 * @since  1.5
 */
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);
}

發現該函數的寫法和Ineger.valueOf(int)很是類似。

咱們一樣也看到,Long也用到了緩存。 使用java.lang.Long#valueOf(long)構造Long對象時,值在[-128, 127]之間的Long對象直接從緩存對象數組中提取。

並且註釋一樣也提到了:緩存的目的是爲了提升性能

可是經過註釋咱們發現這麼一段提示:

Note that unlike the {@linkplain Integer#valueOf(int) corresponding method} in the {@code Integer} class, this method is_not_required to cache values within a particular range.

注意:和Ineger.valueOf(int)不一樣的是,此方法並無被要求緩存特定範圍的值。

這也正是上面源碼中緩存範圍判斷的註釋爲什麼用// will cache的緣由(能夠對比一下上面Integer的緩存的註釋)。

所以咱們可知,雖然此處採用了緩存,但應該不是 JLS 的要求。

那麼Long類型的緩存是如何構造的呢?

咱們查看緩存數組的構造:

private static class LongCache {
    private LongCache(){}

    static final Long cache[] = new Long[-(-128) + 127 + 1];

    static {
        for(int i = 0; i < cache.length; i++)
            cache[i] = new Long(i - 128);
    }
}

能夠看到,它是在靜態代碼塊中填充緩存數組的。

3.2 反編譯

一樣地咱們也編寫一個示例片斷:

public class LongTest {

    public static void main(String[] args) {
        Long a = -128L, b = -128L, c = 150L, d = 150L;
        System.out.println(a == b);
        System.out.println(c == d);
    }
}

編譯源代碼:javac LongTest.java

對編譯後的類文件進行反彙編:javap -c LongTest

獲得下面反編譯的代碼:

public class com.imooc.basic.learn_int.LongTest {
  public com.imooc.basic.learn_int.LongTest();
    Code:
       0: aload_0
       1: invokespecial #1                  // Method java/lang/Object."<init>":()V
       4: return

  public static void main(java.lang.String[]);
    Code:
       0: ldc2_w        #2                  // long -128l
       3: invokestatic  #4                  // Method java/lang/Long.valueOf:(J)Ljava/lang/Long;
       6: astore_1
       7: ldc2_w        #2                  // long -128l
      10: invokestatic  #4                  // Method java/lang/Long.valueOf:(J)Ljava/lang/Long;
      13: astore_2
      14: ldc2_w        #5                  // long 150l
      17: invokestatic  #4                  // Method java/lang/Long.valueOf:(J)Ljava/lang/Long;
      20: astore_3
      21: ldc2_w        #5                  // long 150l
      24: invokestatic  #4                  // Method java/lang/Long.valueOf:(J)Ljava/lang/Long;
      27: astore        4
      29: getstatic     #7                  // Field java/lang/System.out:Ljava/io/PrintStream;
      32: aload_1
      33: aload_2
      34: if_acmpne     41
      37: iconst_1
      38: goto          42
      41: iconst_0
      42: invokevirtual #8                  // Method java/io/PrintStream.println:(Z)V
      45: getstatic     #7                  // Field java/lang/System.out:Ljava/io/PrintStream;
      48: aload_3
      49: aload         4
      51: if_acmpne     58
      54: iconst_1
      55: goto          59
      58: iconst_0
      59: invokevirtual #8                  // Method java/io/PrintStream.println:(Z)V
      62: return
}

咱們從上述代碼中發現Long var = ?的確是經過java.lang.Long#valueOf(long)來構造對象的。

3. 總結

本小節經過源碼分析法、閱讀 JLS 和 JVMS、使用反彙編法,對IntegerLong緩存的目的和實現方式問題進行了深刻分析。

讓你們可以經過更豐富的手段來學習知識和分析問題,經過對緩存目的的思考來學到更通用和本質的東西。

本節使用的幾種手段將是咱們將來經常使用的方法,也是工做進階的必備技能和一個程序員專業程度的體現,但願你們將來可以多動手實踐。

下一節咱們將介紹 Java 序列化相關問題,包括序列化的定義,序列化常見的方案,序列化的坑點等。

4. 課後題

第 1 題:請你們根據今天的研究分析過程,對下面的一個示例代碼進行分析。

public class CharacterTest {
    public static void main(String[] args) {
        Character a = 126, b = 126, c = 128, d = 128;
        System.out.println(a == b);
        System.out.println(c == d);
    }
}

第 2 題: 結合今天的講解,請自行對CharacterShortBoolean的緩存問題進行分析,並比較它們的異同。

參考資料


  1. 阿里巴巴與 Java 社區開發者.《 Java 開發手冊 1.5.0》華山版. 2019. 7↩︎
  2. James Gosling, Bill Joy, Guy Steele, Gilad Bracha, Alex Buckley.《Java Language Specification: Java SE 8 Edition》. 2015↩︎
  3. Tim Lindholm, Frank Yellin, Gilad Bracha, Alex Buckley. 《Java Virtual Machine Specification : Java SE 8 Edition》. 2015↩︎
  4. 周志明.《深刻理解 Java 虛擬機》. 機械工業出版社. 2018↩︎
相關文章
相關標籤/搜索