天天看點

字元串常量池了解

在JVM中,為了減少字元串對象的重複建立,維護了一塊特殊的記憶體空間,這塊記憶體就被稱為字元串常量池。

在JDK1.6及之前,字元串常量池存放在方法區中。到JDK1.7之後,就從方法區中移除了,而存放在堆中。以下是《深入了解Java虛拟機》第二版原文:

對于HotSpot虛拟機,根據官方釋出的路線圖資訊,現在也有放棄永久代并逐漸改為采用Native Memory來實作方法區的規劃了,在目前已經釋出的JDK1.7 的HotSpot中,已經把原本放在永久代的字元串常量池移出。

我們知道字元串常量一般有兩種建立方式:

  1. 使用字元串字面量定義
String s = "aa";
           
  1. 通過new建立字元串對象
String s = new String("aa");
           

那這兩種方式有什麼差別呢?

第一種方式通過字面量定義一個字元串時,JVM會先去字元串常量池中檢查是否存在“aa”這個對象。如果不存在,則在字元串常量池中建立“aa”對象,并将引用傳回給s,這樣s的引用就指向字元串常量池中的“aa”對象。如果存在,則不建立任何對象,直接把常量池中“aa”對象的位址傳回,指派給s。

第二種方式通過new關鍵字建立一個字元串時,我們需要知道建立了幾個對象,這也是面試中經常問到的。首先,會在字元串常量池中建立一個"aa"對象。然後執行new String時會在堆中建立一個“aa”的對象,然後把s的引用指向堆中的這個“aa”對象。

思考以下代碼的列印結果:

public class StringTest {
    public static void main(String[] args) {
        //建立了兩個對象,一份存在字元串常量池中,一份存在堆中
        String s = new String("aa");
        //檢查常量池中是否存在字元串aa,此處存在則直接傳回
        String s1 = s.intern();
        String s2 = "aa";

        System.out.println(s == s2);  //①
        System.out.println(s1 == s2); //②

        String s3 = new String("b")  + new String("b");
        //常量池中沒有bb,在jdk1.7之後會把堆中的引用放到常量池中,故引用位址相等
        String s4 = s3.intern();
        String s5 = "bb";

        System.out.println(s3 == s5 ); //③
        System.out.println(s4 == s5);  //④

    }
}
           

以上的①②③④四個地方應該輸出true還是false呢?别着急,先看下,代碼中用到了intern方法。這個方法的作用是,在運作期間可以把新的常量放入到字元串常量池中。

看下String源碼中對intern方法的解釋:

字元串常量池了解

字面意思就是,當調用這個方法時,會去檢查字元串常量池中是否已經存在這個字元串,如果存在的話,就直接傳回,如果不存在的話,就把這個字元串常量加入到字元串常量池中,然後再傳回其引用。

但是,其實在JDK1.6和 JDK1.7的處理方式是有一些不同的。

在JDK1.6中,如果字元串常量池中已經存在該字元串對象,則直接傳回池中此字元串對象的引用。否則,将此字元串的對象添加到字元串常量池中,然後傳回該字元串對象的引用。

在JDK1.7中,如果字元串常量池中已經存在該字元串對象,則傳回池中此字元串對象的引用。否則,如果堆中已經有這個字元串對象了,則把此字元串對象的引用添加到字元串常量池中并傳回該引用,如果堆中沒有此字元串對象,則先在堆中建立字元串對象,再傳回其引用。(這也說明,此時字元串常量池中存儲的是對象的引用,而對象本身存儲于堆中)

于是代碼中,String s = new String(“aa”);建立了兩個“aa”對象,一個存在字元串常量池中,一個存在堆中。

String s1 = s.intern(); 由于字元串常量池中已經存在“aa”對象,于是直接傳回其引用,故s1指向字元串常量池中的對象。

String s2 = “aa”; 此時字元串常量池中已經存在“aa”對象,是以也直接傳回,故 s2和 s1的位址相同。②傳回true。

System.out.println(s == s2); 由于s的引用指向的是堆中的“aa”對象,s2指向的是常量池中的對象。故不相等,①傳回false。

String s3 = new String(“b”) + new String(“b”); 先說明一下,這種形式的字元串拼接,等同于使用StringBuilder的append方法把兩個“b”拼接,然後調用toString方法,new出“bb”對象,是以“bb”對象是在堆中生成的。是以,這段代碼最終生成了兩個對象,一個是“b”對象存在于字元串常量池中,一個是 “bb”對象,存在于堆中,但是此時字元串常量池中是沒有“bb”對象的。 s3指向的是堆中的“bb”對象。

String s4 = s3.intern(); 調用了intern方法之後,在JDK1.6中,由于字元串常量池中沒有“bb”對象,故建立一個“bb”對象,然後傳回其引用。是以 s4 這個引用指向的是字元串常量池中新建立的“bb”對象。在JDK1.7中,則把堆中“bb”對象的引用添加到字元串常量池中,故s4和s3所指向的對象是同一個,都指向堆中的“bb”對象。

String s5 = “bb”; 在JDK1.6中,指向字元串常量池中的“bb”對象的引用,在JDK1.7中指向的是堆中“bb”對象的引用。

System.out.println(s3 == s5 ); 參照以上分析即可知道,在JDK1.6中③傳回false(因為s3指向的是堆中的“bb”對象,s5指向的是字元串常量池中的“bb”對象),在JDK1.7中,③傳回true(因為s3和s5指向的都是堆中的“bb”對象)。

System.out.println(s4 == s5); 在JDK1.6中,s4和s5指向的都是字元串常量池中建立的“bb”對象,在JDK1.7中,s4和s5指向的都是堆中的“bb”對象。故無論JDK版本如何,④都傳回true。

綜上,在JDK1.6中,傳回的結果為:

false
true
false
true
           

在JDK1.7中,傳回結果為:

false
true
true
true
           

以上,可以在JDK1.7和JDK1.6中分别驗證。注意一下,最好搞兩個項目然後分别設定不同的JDK,因為如果在一個項目中直接更改JDK版本,有可能高版本編譯之後,低版本編譯不通過。

原理搞懂了,我們再思考一下以下代碼的結果:

public class InternTest {
    public static void main(String[] args) {
        String str1 = "xy";
        String str2 = "z";
        String str3 = "xyz";
        String str4 = str1 +  str2;
        String str5 = str4.intern();
        String str6 = "xy" +  "z";

        System.out.println(str3 == str4); //⑤
        System.out.println(str3 == str5); //⑥
        System.out.println(str3 == str6); //⑦
    }
}
           

我們分析一下。

str1、str2和str3都是簡單的定義字元串,所有它們都是在字元串常量池中建立對象,然後引用指向字元串常量池中的對象。