在JVM中,為了減少字符串對象的重復創建,維護了一塊特殊的內存空間,這塊內存就被稱為字符串常量池。
在JDK1.6及之前,字符串常量池存放在方法區中。到JDK1.7之后,就從方法區中移除了,而存放在堆中。以下是《深入理解Java虛擬機》第二版原文:
對於HotSpot虛擬機,根據官方發布的路線圖信息,現在也有放棄永久代並逐步改為采用Native Memory來實現方法區的規划了,在目前已經發布的JDK1.7 的HotSpot中,已經把原本放在永久代的字符串常量池移出。
我們知道字符串常量一般有兩種創建方式:
- 使用字符串字面量定義
String s = "aa";
- 通過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都是簡單的定義字符串,所有它們都是在字符串常量池中創建對象,然后引用指向字符串常量池中的對象。
String str4 = str1 + str2; 這段代碼和之前的 String s3 = new String("b") + new String("b"); 原理相同,因此在堆中創建了一個“xyz”對象,然后str4指向堆中的這個對象。故⑤處返回false。(str3指向的是字符串常量池中的“xyz”對象)
String str5 = str4.intern(); 由於字符串常量池中已經存在“xyz”對象,因此不論是JDK1.6還是JDK1.7,此處返回的都是字符串常量池中對象的引用。所以str5指向字符串常量池中的對象,故 ⑥返回true。
String str6 = "xy" + "z"; 這段代碼需要說明一下,它不同於兩個字符串的引用拼接(如str1 + str2)。JVM會對其優化處理,也就是在編譯階段會把“xy”和“z”進行拼接成為“xyz”,存放在字符串常量池。因此,str6指向的是字符串常量池的對象,故⑦返回true。
