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(則是在堆中,無外乎什么屬性,普通變量所對應的對象內存都是堆中)


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM