天天看點

淺析Java常量池

java中有幾種不同的常量池,以下的内容是對java中幾種常量池的介紹,其中最常見的就是字元串常量池。

(1)class常量池

  在Java中,Java類被編譯後就會形成一份class檔案;class檔案中除了包含類的版本、字段、方法、接口等描述資訊外,還有一項資訊就是常量池,用于存放編譯器生成的各種字面量和符号引用,每個class檔案都有一個class常量池。

  其中字面量包括:1.文本字元串 2.八種基本類型的值 3.被聲明為final的常量等;

  符号引用包括:1.類和方法的全限定名 2.字段的名稱和描述符 3.方法的名稱和描述符。

(2)運作時常量池

  運作時常量池存在于記憶體中,也就是class常量池被加載到記憶體之後的版本,是方法區的一部分(JDK1.8 運作時常量池在元空間,元空間也是方法區的一種實作)。不同之處是:它的字面量可以動态的添加(String類的intern()),符号引用可以被解析為直接引用。

  JVM在執行某個類的時候,必須經過加載、連接配接、初始化,而連接配接又包括驗證、準備、解析三個階段。而當類加載到記憶體中後,jvm就會将class常量池中的内容存放到運作時常量池中,這裡所說的常量包括:基本類型包裝類(包裝類不管理浮點型,整形隻會管理-128到127)和字元串類型(即通過String.intern()方法可以強制将String放入常量池),運作時常量池是每個類私有的。在解析階段,會把符号引用替換為直接引用。

(3)基本類型包裝類常量池

  Java 基本類型的包裝類的大部分都實作了常量池技術。Byte,Short,Integer,Long

 這 4 種包裝類預設建立了數值 [-128,127] 的相應類型的緩存資料,Character建立了數值在[0,127]範圍的緩存資料,Boolean直接傳回True或False,如果超出對應範圍就會去建立新的對象。兩種浮點數類型的包裝類Float,Double并沒有實作常量池技術。

  Integer 緩存源碼:

/**

*此方法将始終緩存-128 到 127(包括端點)範圍内的值,并可以緩存此範圍之外的其他值。

*/

public static Integer valueOf(int i) {

    if (i >= IntegerCache.low && i <= IntegerCache.high)

      return IntegerCache.cache[i + (-IntegerCache.low)];

    return new Integer(i);

}

private static class IntegerCache {

    static final int low = -128;

    static final int high;

    static final Integer cache[];

}      

  舉個栗子:

Integer i1 = 40;
  Integer i2 = 40;
  Integer i3 = 0;
  Integer i4 = new Integer(40);
  Integer i5 = new Integer(40);
  Integer i6 = new Integer(0);

  System.out.println("i1=i2   " + (i1 == i2));
  System.out.println("i1=i2+i3   " + (i1 == i2 + i3));
  System.out.println("i1=i4   " + (i1 == i4));
  System.out.println("i4=i5   " + (i4 == i5));
  System.out.println("i4=i5+i6   " + (i4 == i5 + i6));   
  System.out.println("40=i5+i6   " + (40 == i5 + i6));      

  結果:

i1=i2         true
i1=i2+i3   true
i1=i4        false
i4=i5        false
i4=i5+i6   true
40=i5+i6   true      

  解釋:1-4語句結果應該很顯然,因為

Integer i1=40

這一行代碼會發生裝箱,也就是說這行代碼等價于

Integer i1=Integer.valueOf(40)

 ,

Integer.valueOf()

 方法基于減少對象建立次數和節省記憶體的考慮,緩存了[-128,127]之間的數字,如果在此數字範圍内直接傳回緩存中的對象。在此之外,直接new出來,顯然40在常量池的緩存[-128,127]範圍内;是以,

i1

直接使用的是常量池中的對象。而

Integer i1 = new Integer(40)

會直接建立新的對象;語句 i4 == i5 + i6,因為+這個操作符不适用于 Integer 對象,首先 i5 和 i6 進行自動拆箱操作,進行數值相加,即 i4 == 40。然後 Integer 對象無法與數值進行直接比較,是以 i4 自動拆箱轉為 int 值 40,最終這條語句轉為 40 == 40 進行數值比較,是以結果為true。第六條語句同理。

額外說明:所有整型包裝類對象之間值的比較,全部使用 equals 方法比較。

  對于Integer var = ?在-128至127之間的指派,Integer對象是在 IntegerCache.cache産生,會複用已有對象,這個區間内的Integer值可以直接使用==進行判斷,但是這個區間之外的所有資料,都會在堆上産生,并不會複用已有對象,推薦使用equals方法進行判斷。

(4)字元串常量池

  在JDK1.6及之前版本,字元串常量池存放在方法區中的,在JDK1.7版本以後,字元串常量池被移到了堆中了。

  HotSpot VM裡,記錄interned string的一個全局表叫做StringTable,它本質上就是個HashSet<String>;這個StringTable在每個HotSpot VM的執行個體隻有一份,被所有的類共享。

  注意:它隻存儲對java.lang.String執行個體的引用,而不存儲String對象的内容

字元串常量池和上面的基本類型包裝類常量池有些不同,字元串常量池中沒有事先緩存一些資料,而是如果要建立的字元串在常量池記憶體在就傳回對象的引用,如果不存在就建立一個放在常量池中;

在Java中,有兩種建立字元串對象的方法,一種是字面量直接建立,另一種是new一個String對象,這兩種方法建立字元串對象的過程會不一樣;

(1)String str = "abc";

(2)String str = new String("abc");      

如果是第一種方式建立對象,因為是字面量直接建立,是以在編譯的時候是确定的,如果該字元串不在常量池中會将該字元串放入常量池中并傳回字元串對象的引用,如果在常量池中直接傳回字元串對象的引用,如果是第二種方式建立對象,因為要建立String類型的對象,String對象是在運作時才加載到記憶體的堆中的,屬于運作時建立,是以要先在堆中建立一個String對象,再去常量池中尋找是否有相同的字元串,如果有就傳回堆中Sring對象的引用,如果沒有則在将該字元串加入常量池中。

舉個栗子:

比較下列兩種建立字元串的方法:

String str1 = new String("abc");

String str2 = "abc";

答案:第一種是用new()來建立對象的,它會在存放于堆中。每調用一次就會建立一個新的對象。 運作時期建立 。

第二種是先在棧中建立一個對String類的對象引用變量str2,然後通過符号引用去字元串常量池裡找有沒有”abc”,如果沒有,則将”abc”存放進字元串常量池,并令str2指向”abc”,如果已經有”abc” 則直接令str2指向“abc”。“abc”存于常量池在 編譯期間完成 。

String s = new String("abc")

這條語句建立了幾個對象?

答案:共2個。第一個對象是”abc”字元串存儲在常量池中,第二個對象在Java Heap中的 String 對象。這裡不要混淆了s是放在棧裡面的指向了Heap堆中的String對象。

String s1 = new String("s1") ;

String s1 = new String("s1") ;

上面一共建立了幾個對象?

答案:答案:3個 ,編譯期常量池中建立1個,運作期堆中建立2個.(用new建立的每new一次就在堆上建立一個對象,用引号建立的如果在常量池中已有就直接指向,不用建立)

淺析Java常量池