聊一聊Integer的快取機制問題

码农Academy發表於2024-03-06

在Java程式設計中,Integer類作為基本型別int的包裝器,提供了物件化的操作和自動裝箱與拆箱的功能。從JDK5開始引入了一項特別的最佳化措施——Integer快取機制,它對於提升程式效能和減少記憶體消耗具有重要意義。接下來我們由一段程式碼去開啟Integer快取機制的秘密。

public static void main(String[] args) {  
    Integer i1 = 100;  
    Integer i2 = 100;  
    System.out.println(i1 == i2);  
    Integer i3 = 1000;  
    Integer i4 = 1000;  
    System.out.println(i3 == i4);  
}

至於答案是什麼呢?我們接著往下看,等你看完就明白了。

當你在你的Idea中寫出這段程式碼的時候,Idea就會提示你要使用equals()方法區比較大小,因為Integer是物件,物件的值比較要用equals()方法,而不是使用==,這裡我們主要是研究一下Integer的快取機制。

Integer快取是什麼

Java的Integer類內部實現了一個靜態快取池,用於儲存特定範圍內的整數值對應的Integer物件。預設情況下,這個範圍是-128至127。當透過Integer.valueOf(int)方法建立一個在這個範圍內的整數物件時,並不會每次都生成新的物件例項,而是複用快取中的現有物件。我們看一下Integer.valueOf(int)的原始碼:

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

對於Integer.valueOf(int)方法來說,由於這個方法經常用於將基本型別int轉換為包裝器物件,所以它使用了@HotSpotIntrinsicCandidate註解,這樣HotSpot JVM可能會提供一種更為高效的內部實現來處理自動裝箱操作。而IntegerCacheInteger內部的一個靜態類,負責快取整數物件。它在類載入時被初始化,建立並快取範圍內的所有整數物件。我們看一下IntegerCache的原始碼:

private static class IntegerCache {  
    // 快取範圍的下限,預設為-128  
    static final int low = -128;  
    // 快取範圍的上限,初始化時動態計算(基於系統屬性或預設值127)  
    static final int high;  
    // 儲存在快取範圍內所有Integer物件的陣列  
    static final Integer cache[];  
    // 靜態初始化塊,在類載入時執行  
    static {  
        // 初始設定high為127  
        int h = 127;  
        // 嘗試從系統屬性獲取使用者自定義的最大整數值  
        String integerCacheHighPropValue =  
                VM.getSavedProperty("java.lang.Integer.IntegerCache.high");  
        // 如果系統屬性存在並且可以轉換為int型別,則更新high值  
        if (integerCacheHighPropValue != null) {  
            try {  
                int i = parseInt(integerCacheHighPropValue);  
                // 確保high至少為127,並且不超過Integer.MAX_VALUE允許的最大陣列大小  
                h = Math.max(i, 127);  
                h = Math.min(i, Integer.MAX_VALUE - (-low) -1);  
            } catch( NumberFormatException nfe) {  
            }  
        }  
  
        // 設定最終確定的high值  
        high = h;  
        // 初始化cache陣列,長度等於快取範圍內的整數數量  
        cache = new Integer[(high - low) + 1];  
        // 使用迴圈填充cache陣列,建立並儲存對應的Integer物件  
        int j = low;  
        for(int k = 0; k < cache.length; k++) {  
            cache[k] = new Integer(j++);  
        }  
        // 檢查,確保快取範圍至少包含[-128, 127]  
        // 這是Java語言規範對小整數自動裝箱共享的要求  
        assert IntegerCache.high >= 127;  
    }  
    // 私有構造器,防止外部例項化此內部類的物件  
    private IntegerCache() {}  
}

IntegerCache類在Java虛擬機器啟動時建立了一個固定大小的陣列,用於快取指定範圍內所有的Integer物件。這樣在後續程式執行過程中,對於這些範圍內的整數進行裝箱操作時,可以直接從快取中獲取已存在的物件,以提升效能並減少記憶體開銷。同時,它也提供了根據系統屬性(-Djava.lang.Integer.IntegerCache.high)來自定義快取上限的能力,並確保滿足Java語言規範關於小整數自動裝箱共享的規定。

Integer.value(int)方法中,如果int的值在IntegerCache返回的lowhigh之內,則直接返回IntegerCache中快取的物件,否則重新new一個新的Integer物件。

而文章開頭示例中,我們使用Interge i1 = 100的方式其實是Java的自動裝箱機制,整數字面量100是一個基本型別的int值。當賦值給一個Integer引用變數i時,編譯器會隱式地呼叫Integer.valueOf(int)方法將這個基本型別的int值轉換為Integer物件。

整數在程式設計中經常被使用,特別是在迴圈計數等場景中,透過快取整數物件,可以大幅度減少相同整數值的物件建立,從而減小記憶體佔用。

由此我們可以看出因為100在[-128, 127]之內,所以i1 == i2列印true,而1000不在[-128, 127]之內,所以i3 == i4列印false
image.png

我們嘗試使用java.lang.Integer.IntegerCache.high調整一下high為1000,然後看一下效果:
image.png
image.png
列印結果都是true。

當然這個上限不要隨意去調整,調整之前,需要仔細評估應用程式的實際需求和效能影響。儘量選擇在[-128, 127]範圍內的整數值,以充分利用Integer快取機制。

注意事項

  • 比較: 由於快取的存在,在-128至127之間的Integer物件在進行==運算子比較時,結果可能是true,因為它們指向的是同一個記憶體地址。而在快取範圍之外建立的Integer物件即使值相等,也會視為不同的物件,因此使用==比較會返回false。不論是否啟用快取,對於任何兩個Integer物件,只要其包含的整數值相同,呼叫equals()方法始終會返回true。所以我們在比較物件時一定要使用equals()方法。

  • 不適用於所有場景: 當使用new Integer(i)直接建立Integer物件時,不會利用快取。

  • 不要隨意去擴充套件快取的上下限

總結

Integer快取機制是Java中的一項效能最佳化措施,透過快取一定範圍內的整數物件,既能減小記憶體開銷,又能提高效能。

本文已收錄於我的個人部落格:碼農Academy的部落格,專注分享Java技術乾貨,包括Java基礎、Spring Boot、Spring Cloud、Mysql、Redis、Elasticsearch、中介軟體、架構設計、面試題、程式設計師攻略等

相關文章