1. 程式人生 > >IntegerCache快取佔用堆、棧、常量池的問題,自動拆裝箱的基本概念,Integer==int時的問題說明

IntegerCache快取佔用堆、棧、常量池的問題,自動拆裝箱的基本概念,Integer==int時的問題說明

原創宣告:作者:Arnold.zhao 部落格園地址:https://www.cnblogs.com/zh94

先普及一個基本概念:Java中基本資料型別的裝箱和拆箱操作

自動裝箱

在JDK5以後,我們可以直接使用Integer num = 2;來進行值的定義,但是你有沒有考慮過?Integer是一個物件呀,為什麼我可以不例項化物件,就直接來進行Value的定義呢?

一般情況下我們在定義一個物件的時候,頂多賦值為一個null 即空值;
比如:Person pserson = null;但是肯定不可以Person person =2;這樣操作吧,
那為什麼Integer,Float,Double,等基本資料型別的包裝類是可以直接定義值的呢?

究其原因無非是編譯器在編譯程式碼的時候,重新進行了一次例項化的操作而已啦:
比如當我們使用Integer num = 2 的時候,在JVM執行前的編譯階段,此時該Integer num = 2 將會被編譯為
Integer num = new Integer(2); 那麼此時編譯後的這樣一個語法 new Integer(2) 則是符合JDK執行時的規則的,而這種操作就是所謂的裝箱操作;

注意:(不要拿Integer和int型別來進行對比,int,float,這些是JDK自定義的關鍵字,
本身在編譯的時候就會被特殊處理,而Integer,Float,Double等則是標準的物件,物件的實現本身就是要有new 的操作才是合理;

所以對於這些基本型別的包裝類在進行 Integer num = 2的賦值時,則的確是必須要有一個裝箱的操作將其變成物件例項化的方式這樣也才是一個標準的過程;)

自動拆箱

那麼當你瞭解了對應的裝箱操作後,再來了解一下對應拆箱的操作:

當我們把一個原本的Integer num1 = 2; 來轉換為 int num1 = 2的時候實際上就是一個拆箱的操作,及把包裝型別轉換為基本資料型別時便是所謂的拆箱操作;
一般當我們進行對比的時候,編譯器便會優先把包裝類進行自動拆箱:如Integer num1 = 2 和 int num2 = 2;當我們進行對比時
if(num1 == num2) 那麼此時編譯器便會自動的將包裝類的num1自動拆箱為int型別進行對比等操作;

裝箱及拆箱時的真正步驟

上述已經說過了自動裝箱時,實際上是把 Integer num =2 編譯時變更為了 Integer num = new Integer(2);
但實際上JDK真的就只是這麼簡單的進行了一下new的操作嗎?當然不是,在自動裝箱的過程中實際上是呼叫的Integer的valueOf(int i)的方法,來進行的裝箱的操作;
我們來看一下這個方法的具體實現:我會直接在下述原始碼中加註釋,直接看註釋即可

    public static Integer valueOf(int i) {
            //在呼叫valueOf進行自動裝箱時,會先進行一次所傳入值的判斷,當i的值大於等於IntegerCache.low 以及 小於等於IntegerCache.high時,則直接從已有的IntegerCache.cache中取出當前元素return即可;
            if (i >= IntegerCache.low && i <= IntegerCache.high){
                            return IntegerCache.cache[i + (-IntegerCache.low)];
            }
            //否則則直接new Integer(i) 例項化一個新的Integer物件並return出去;
            return new Integer(i);
    }
    //此時我們再看一下上述的IntegerCache到底是做的什麼操作,如下類:(注意:此處IntegerCache是 private 內部靜態類,所以我們定義的外部類是無法直接使用的,此處看原始碼即可)
    
    private static class IntegerCache {
            //定義一個low最低值 及 -128;
            static final int low = -128;
            //定義一個最大值(最大值的初始化詳情看static程式碼塊)
            static final int high;
            //定義一個Integer陣列,陣列中儲存的都是 new Integer()的資料;(陣列的初始化詳情看static程式碼塊)
            static final Integer cache[];
    
            static {
                //此處定義一個預設的值為127;
                int h = 127;
                //sun.misc.VM.getSavedProperty() 表示從JVM引數中去讀取這個"java.lang.Integer.IntegerCache.high"的配置,並賦值給integerCacheHighPropValue變數
                String integerCacheHighPropValue = sun.misc.VM.getSavedProperty("java.lang.Integer.IntegerCache.high");
                //當從JVM中所取出來的這個java.lang.Integer.IntegerCache.high值不為空時
                if (integerCacheHighPropValue != null) {
                    try {
                        //此處將JVM所讀取出的integerCacheHighPropValue值進行parseInt的轉換並賦值給 int i;
                        int i = parseInt(integerCacheHighPropValue);
                        //Math.max()方法含義是,當i值大於等於127時,則輸出i值,否則則輸出 127;並賦值給 i;
                        i = Math.max(i, 127);
                        //Math.min()則表示,當 i值 小於等於 Integer.MAX_VALUE時,則輸出 i,否則輸出 Integer.MAX_VALUE,並賦值給 h
                        //此處使用:Integer.MAX_VALUE - (-low) -1 的原因是由於是從負數開始的,避免Integer最大值溢位,所以這樣寫的,此處可以先不考慮
                        h = Math.min(i, Integer.MAX_VALUE - (-low) -1);
                    } catch( NumberFormatException nfe) {
                        // If the property cannot be parsed into an int, ignore it.
                    }
                }
                //最後把所得到的最終結果 h 賦值給我們親愛的 high 屬性;
                high = h;
    
                //以下賦值當前cache陣列的最大長度;
                cache = new Integer[(high - low) + 1];
                int j = low;
                //然後進行cache陣列的初始化迴圈;
                for(int k = 0; k < cache.length; k++)
                    //注意:此處new Integer(j++);是先例項化的j,也就是負數-128,所以也才會有上述的Integer.MAX_VALUE - (-low) -1)的操作,因為陣列中儲存的是 -128 到 high 的所有例項化資料物件;
                    cache[k] = new Integer(j++);
    
                // range [-128, 127] must be interned (JLS7 5.1.7)
                assert IntegerCache.high >= 127;
            }
    
            private IntegerCache() {}
        }

朋友們,由上述的程式碼我們便可以知道,自動裝箱時:

1、high的值如果未通過JVM引數定義時則預設是127,當通過JVM引數進行定義後,則使用所定義的high值,前提是不超出(Integer.MAX_VALUE - (-low) -1)的長度即可,如果超出這個長度則預設便是:Integer.MAX_VALUE - (-low) -1;

2、預設情況下會儲存一個 -128 到 high的 Integer cache[]陣列,並且已經例項化了所有 -128 到high的Integer物件資料;

3、當使用valueOf(int i)來自動裝箱時,會先判斷一下當前所需裝箱的值是否(大於等於IntegerCache.low && 小於等於IntegerCache.high) 如果是,則直接從當前已經全域性初始化好的cache陣列中返回即可,如果不是則重新 new Integer();

而當Integer物件在自動拆箱時則是呼叫的Integer的intValue()方法,方法程式碼如下:可以看出是直接把最初的int型別的value值直接返回了出去,並且此時返回的只是基本資料型別!

    private final int value;

    public int intValue() {
        return value;
    }

所以,朋友們,讓我們帶著上述的答案,來看下我們常在開發程式碼時碰到的一些問題:(請接著向下看哦,因為最後還會再涉及到一些JVM的說明哦)

原創宣告:作者:Arnold.zhao 部落格園地址:https://www.cnblogs.com/zh94

Integer於Int進行==比較時的程式碼案例

 public static void main(String[] args) {
        Integer num1 = 2000;
        int num2 = 2000;
        //會將Integer自動拆箱為int比較,此處為true;因為拆箱後便是 int於int比較,不涉及到記憶體比較的問題;
        System.out.println(num1 == num2);
        Integer num3 = new Integer(2000);
        Integer num4 = new Integer(2000);
        //此處為false,因為 num3 是例項化的一個新物件對應的是一個新的記憶體地址,而num4也是新的記憶體地址;
        System.out.println(num3 == num4);
        Integer num5 = 100;
        Integer num6 = 100;
        //返回為true,因為Integer num5 =100的定義方式,會被自動呼叫valueOf()進行裝箱;而valueOf()裝箱時是一個IntegerCache.high的判斷的,只要在這個區間,則直接return的是陣列中的元素
        //而num5 =100 及返回的是陣列中下標為100的物件,而num6返回的也是陣列中下標為 100的物件,所以兩個物件是相同的物件,此時進行 == 比較時,記憶體地址相同,所以為true
        System.out.println(num5 == num6);
        Integer num7 = new Integer(100);
        Integer num8 = 100;
        //結果為false;為什麼呢?因為num7並不是自動裝箱的結果,而是自己例項化了一個新的物件,那麼此時便是堆裡面新的記憶體地址,而num8儘管是自動裝箱,但返回的物件與num7的物件也不是一個記憶體地址哦;
        System.out.println(num7 == num8);
    }

原創宣告:作者:Arnold.zhao 部落格園地址:https://www.cnblogs.com/zh94

總結

  • 1、由於我們在使用Integer和int進行==比較時,存在著自動拆箱於裝箱的操作,所以在程式碼中進行Integer的對比時儘可能的使用 .equals()來進行對比;
    比如我們定義如下一個方法:那麼我們此時是無法知曉num1 和num2的值是否是直接new出來的?還是自動裝箱定義出來的?就算兩個值都是自動裝箱定義出來的,那麼num1 和num2的值是否超出了預設的-128到127的cache陣列快取呢?如果超出了那麼還是new 的Integer(),此時我們進行 == 對比時,無疑是風險最大的,所以最好的還是 .equals()進行對比;除非是拿一個Integer和一個int基本型別進行對比可以使用==,因為此時無論Integer是新new例項化的還是自動裝箱的,在對比時都會被自動拆箱為 int基本資料型別進行對比;
    public void test(Integer num1,Integer num2){
        //TODO
    }
  • 2、合理的在專案上線後,使用-XX:AutoBoxCacheMax=20000 引數來定義自動裝箱時的預設最大high值,可以很好的避免基本資料型別包裝類被頻繁堆內建立的問題;什麼個意思呢,一般情況下我們在專案開發過程中,會大量使用Integer num = 23;等等的程式碼,並且我們在操作資料庫的時候,一般返回的Entity實體類裡面也會定義一大堆的Integer型別的屬性,而上述也提到過了,每次Integer的使用實際上都會被自動裝箱,對於超出-128和127的值,則會被建立新的堆物件;所以如果我們有很多的大於127的資料值,那麼每次都需要在堆中建立臨時物件豈不是一個很可惜的操作嗎,如果我們在專案啟動時設定-XX:AutoBoxCacheMax=20000,那麼對於我們常用的Integer為2W以下的數字,則直接從IntegerCache 陣列中直接取就行了,完全就沒必要再建立臨時的堆物件了嘛;這樣對於整個JVM的GC回收來說,多多少少也是一些易處呀,避免了大量的重複的Integer物件的建立佔用和回收的問題呢;不是嘛

  • 3、之前在本人還是初初初級,初出茅廬程式猿的時候,就經常聽到有的人說,JVM中關於-128到127的cache快取是存在常量池裡面的,有的人說當你在定義int型別時實際上是儲存在棧裡面的,搞的我也是很尷尬呀;很難抉擇,
    那麼現在呢,就給出一個最終的本人總結後的答案,如下:

  • 首先我們看了上述自動裝箱的原始碼以後,可以知道,初始化的快取資料是定義在靜態屬性中的:static final Integer cache[]; 所以,答案是:我們自動裝箱的cache陣列快取的確是定義在常量池中的;每次我們自動裝箱時的陣列判斷,的確是從常量池中拿的資料,
    廢話,因為是 static final 型別的呀,所以當然是常量池中儲存的cache陣列啦

  • 但是:關於int型別中定義的變數實際上是儲存於棧空間的,這個也是沒錯的,因為關於JVM棧中有一個定義是:針對區域性變數中的基本型別的字面量則是儲存線上程棧中的;(棧是執行緒的一個數據結構),
    所以對於我們在方法中定義的區域性變數:int a = 3 時,則的確是儲存線上程棧中的;而我們在方法中定義區域性變數 Integer a=300時,這個肯定是在堆或者常量池中啦(看是否自動裝箱後使用常量池中cache);

  • 而對於我們在類中定義的成員屬性來說,比如:static int a =3;此時則是在常量池中(無外乎什麼型別因為他是靜態的,所以常量池)而類的成員屬性 int a=3(則是在堆中,無外乎什麼屬性,普通變數所對應的物件記憶體都是堆中)