在JVM中系任,為了減少字符串對象的重復創(chuàng)建,維護了一塊特殊的內存空間虐块,這塊內存就被稱為字符串常量池俩滥。
在JDK1.6及之前,字符串常量池存放在方法區(qū)中贺奠。到JDK1.7之后霜旧,就從方法區(qū)中移除了,而存放在堆中儡率。以下是《深入理解Java虛擬機》第二版原文:
對于HotSpot虛擬機挂据,根據官方發(fā)布的路線圖信息,現在也有放棄永久代并逐步改為采用Native Memory來實現方法區(qū)的規(guī)劃了儿普,在目前已經發(fā)布的JDK1.7 的HotSpot中崎逃,已經把原本放在永久代的字符串常量池移出。
我們知道字符串常量一般有兩種創(chuàng)建方式:
- 使用字符串字面量定義
String s = "aa";
- 通過new創(chuàng)建字符串對象
String s = new String("aa");
那這兩種方式有什么區(qū)別呢眉孩?
第一種方式通過字面量定義一個字符串時个绍,JVM會先去字符串常量池中檢查是否存在“aa”這個對象。如果不存在浪汪,則在字符串常量池中創(chuàng)建“aa”對象障贸,并將引用返回給s,這樣s的引用就指向字符串常量池中的“aa”對象吟宦。如果存在篮洁,則不創(chuàng)建任何對象,直接把常量池中“aa”對象的地址返回殃姓,賦值給s袁波。
第二種方式通過new關鍵字創(chuàng)建一個字符串時瓦阐,我們需要知道創(chuàng)建了幾個對象,這也是面試中經常問到的篷牌。首先睡蟋,會在字符串常量池中創(chuàng)建一個"aa"對象。然后執(zhí)行new String時會在堆中創(chuàng)建一個“aa”的對象枷颊,然后把s的引用指向堆中的這個“aa”對象戳杀。
思考以下代碼的打印結果:
public class StringTest {
public static void main(String[] args) {
//創(chuàng)建了兩個對象,一份存在字符串常量池中夭苗,一份存在堆中
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中爹袁,如果字符串常量池中已經存在該字符串對象远荠,則返回池中此字符串對象的引用。否則失息,如果堆中已經有這個字符串對象了譬淳,則把此字符串對象的引用添加到字符串常量池中并返回該引用档址,如果堆中沒有此字符串對象,則先在堆中創(chuàng)建字符串對象邻梆,再返回其引用守伸。(這也說明,此時字符串常量池中存儲的是對象的引用浦妄,而對象本身存儲于堆中)
于是代碼中尼摹,String s = new String("aa");創(chuàng)建了兩個“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”對象逊抡,故創(chuàng)建一個“bb”對象,然后返回其引用零酪。所以 s4 這個引用指向的是字符串常量池中新創(chuàng)建的“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指向的都是字符串常量池中創(chuàng)建的“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都是簡單的定義字符串蒂培,所有它們都是在字符串常量池中創(chuàng)建對象再愈,然后引用指向字符串常量池中的對象。
String str4 = str1 + str2; 這段代碼和之前的 String s3 = new String("b") + new String("b"); 原理相同护戳,因此在堆中創(chuàng)建了一個“xyz”對象翎冲,然后str4指向堆中的這個對象。故⑤處返回false媳荒。(str3指向的是字符串常量池中的“xyz”對象)
String str5 = str4.intern(); 由于字符串常量池中已經存在“xyz”對象抗悍,因此不論是JDK1.6還是JDK1.7驹饺,此處返回的都是字符串常量池中對象的引用。所以str5指向字符串常量池中的對象缴渊,故 ⑥返回true赏壹。
String str6 = "xy" + "z"; 這段代碼需要說明一下,它不同于兩個字符串的引用拼接(如str1 + str2)衔沼。JVM會對其優(yōu)化處理蝌借,也就是在編譯階段會把“xy”和“z”進行拼接成為“xyz”,存放在字符串常量池指蚁。因此菩佑,str6指向的是字符串常量池的對象,故⑦返回true凝化。