通過反編譯深入理解Java String及intern + JDK1.8關(guān)于運(yùn)行時(shí)常量池, 字符串常量池的要點(diǎn)

一秤茅、字符串問題

字符串在我們平時(shí)的編碼工作中其實(shí)用的非常多璧亚,并且用起來也比較簡單砰识,所以很少有人對其做特別深入的研究。倒是面試或者筆試的時(shí)候块茁,往往會(huì)涉及比較深入和難度大一點(diǎn)的問題齿坷。我在招聘的時(shí)候也偶爾會(huì)問應(yīng)聘者相關(guān)的問題,倒不是說一定要回答的特別正確和深入数焊,通常問這些問題的目的有兩個(gè)永淌,第一是考察對 JAVA 基礎(chǔ)知識(shí)的了解程度,第二是考察應(yīng)聘者對技術(shù)的態(tài)度佩耳。

我們看看以下程序會(huì)輸出什么結(jié)果遂蛀?如果你能正確的回答每一道題,并且清楚其原因干厚,那本文對你就沒什么太大的意義李滴。如果回答不正確或者不是很清楚其原理螃宙,那就仔細(xì)看看以下的分析,本文應(yīng)該能幫助你清楚的理解每段程序的結(jié)果及輸出該結(jié)果的深層次原因悬嗓。

代碼段一:

package com.paddx.test.string;
public class StringTest {
    public static void main(String[] args) {
        String str1 = "string";
        String str2 = new String("string");
        String str3 = str2.intern();

        System.out.println(str1==str2);//#1
        System.out.println(str1==str3);//#2
    }
}

代碼段二:

package com.paddx.test.string;
public class StringTest01 {
    public static void main(String[] args) {
        String baseStr = "baseStr";
        final String baseFinalStr = "baseStr";

        String str1 = "baseStr01";
        String str2 = "baseStr"+"01";
        String str3 = baseStr + "01";
        String str4 = baseFinalStr+"01";
        String str5 = new String("baseStr01").intern();

        System.out.println(str1 == str2);//#3
        System.out.println(str1 == str3);//#4
        System.out.println(str1 == str4);//#5
        System.out.println(str1 == str5);//#6
    }
}

代碼段三(1):

package com.paddx.test.string;<br>  
public class InternTest {
    public static void main(String[] args) {

        String str2 = new String("str")+new String("01");
        str2.intern();
        String str1 = "str01";
        System.out.println(str2==str1);//#7
    }
}

代碼段三(2):

package com.paddx.test.string;

public class InternTest01 {
    public static void main(String[] args) {
        String str1 = "str01";
        String str2 = new String("str")+new String("01");
        str2.intern();
        System.out.println(str2 == str1);//#8
    }
}

為了方便描述,我對上述代碼的輸出結(jié)果由#1~#8進(jìn)行了編碼裕坊,下文中藍(lán)色字體部分即為結(jié)果包竹。

二、字符串深入分析

  1籍凝、代碼段一分析

字符串不屬于基本類型周瞎,但是可以像基本類型一樣,直接通過字面量賦值饵蒂,當(dāng)然也可以通過new來生成一個(gè)字符串對象声诸。不過通過字面量賦值的方式和new的方式生成字符串有本質(zhì)的區(qū)別:

image

通過字面量賦值創(chuàng)建字符串時(shí),會(huì)優(yōu)先在常量池中查找是否已經(jīng)存在相同的字符串退盯,倘若已經(jīng)存在彼乌,棧中的引用直接指向該字符串;倘若不存在渊迁,則在常量池中生成一個(gè)字符串慰照,再將棧中的引用指向該字符串。而通過new的方式創(chuàng)建字符串時(shí)琉朽,就直接在堆中生成一個(gè)字符串的對象(備注毒租,JDK 7 以后,HotSpot 已將常量池從永久代轉(zhuǎn)移到了堆中箱叁。詳細(xì)信息可參考《JDK8內(nèi)存模型-消失的PermGen》一文)墅垮,棧中的引用指向該對象。對于堆中的字符串對象耕漱,可以通過 intern() 方法來將字符串添加的常量池中算色,并返回指向該常量的引用。

現(xiàn)在我們應(yīng)該能很清楚代碼段一的結(jié)果了:

結(jié)果 #1:因?yàn)閟tr1指向的是字符串中的常量螟够,str2是在堆中生成的對象剃允,所以str1==str2返回false。

結(jié)果 #2:str2調(diào)用intern方法齐鲤,會(huì)將str2中值(“string”)復(fù)制到常量池中斥废,但是常量池中已經(jīng)存在該字符串(即str1指向的字符串),所以直接返回該字符串的引用给郊,因此str1==str2返回true牡肉。

以下運(yùn)行代碼段一的代碼的結(jié)果:

image

** 2、代碼段二分析**

對于代碼段二的結(jié)果淆九,還是通過反編譯StringTest01.class文件比較容易理解:

常量池內(nèi)容(部分):

image

執(zhí)行指令(部分统锤,第二列#+序數(shù)對應(yīng)常量池中的項(xiàng)):

image

在解釋上述執(zhí)行過程之前毛俏,先了解兩條指令:

ldc:Push item from run-time constant pool,從常量池中加載指定項(xiàng)的引用到棧饲窿。

astore_<n>:Store reference into local variable煌寇,將引用賦值給第n個(gè)局部變量。

現(xiàn)在我們開始解釋代碼段二的執(zhí)行過程:

0: ldc #2:加載常量池中的第二項(xiàng)("baseStr")到棧中逾雄。

2: astore_1 :將1中的引用賦值給第一個(gè)局部變量阀溶,即String baseStr = "baseStr";

3: ldc #2:加載常量池中的第二項(xiàng)("baseStr")到棧中鸦泳。

5: astore_2 :將3中的引用賦值給第二個(gè)局部變量银锻,即 final String baseFinalStr="baseStr";

6: ldc #3:加載常量池中的第三項(xiàng)("baseStr01")到棧中做鹰。

8: astore_3 :將6中的引用賦值給第三個(gè)局部變量击纬,即String str1="baseStr01";

9: ldc #3:加載常量池中的第三項(xiàng)("baseStr01")到棧中。

11: astore 4:將9中的引用賦值給第四個(gè)局部變量:即String str2="baseStr01"钾麸;

結(jié)果#3:str1==str2 肯定會(huì)返回true更振,因?yàn)閟tr1和str2都指向常量池中的同一引用地址。所以其實(shí)在JAVA 1.6之后饭尝,常量字符串的“+”操作殃饿,編譯階段直接會(huì)合成為一個(gè)字符串。

13: new #4:生成StringBuilder的實(shí)例芋肠。

16: dup      :復(fù)制13生成對象的引用并壓入棧中乎芳。

17: invokespecial #5:調(diào)用常量池中的第五項(xiàng),即StringBuilder.<init>方法帖池。

以上三條指令的作用是生成一個(gè)StringBuilder的對象奈惑。

20: aload_1  :加載第一個(gè)參數(shù)的值睡汹,即"baseStr"

21: invokevirtual #6 :調(diào)用StringBuilder對象的append方法肴甸。

24: ldc #7:加載常量池中的第七項(xiàng)("01")到棧中。

26: invokevirtual #6:調(diào)用StringBuilder.append方法囚巴。

29: invokevirtual #8:調(diào)用StringBuilder.toString方法原在。

32: astore 5:將29中的結(jié)果引用賦值改第五個(gè)局部變量,即對變量str3的賦值彤叉。

結(jié)果 #4:因?yàn)閟tr3實(shí)際上是stringBuilder.append()生成的結(jié)果庶柿,所以與str1不相等,結(jié)果返回false秽浇。

34: ldc #3:加載常量池中的第三項(xiàng)("baseStr01")到棧中浮庐。

36: astore 6:將34中的引用賦值給第六個(gè)局部變量,即str4="baseStr01";

結(jié)果 #5 :因?yàn)閟tr1和str4指向的都是常量池中的第三項(xiàng)柬焕,所以str1==str4返回true审残。這里我們還能發(fā)現(xiàn)一個(gè)現(xiàn)象梭域,對于final字段,編譯期直接進(jìn)行了常量替換搅轿,而對于非final字段則是在運(yùn)行期進(jìn)行賦值處理的病涨。

38: new #9:創(chuàng)建String對象

41: dup :復(fù)制引用并壓入棧中。

42: ldc #3:加載常量池中的第三項(xiàng)("baseStr01")到棧中璧坟。

44: invokespecial #10:調(diào)用String."<init>"方法既穆,并傳42步驟中的引用作為參數(shù)傳入該方法。

47: invokevirtual #11:調(diào)用String.intern方法沸柔。

從38到41的對應(yīng)的源碼就是new String("baseStr01").intern()循衰。

50: astore 7:將47步返回的結(jié)果賦值給變量7铲敛,即str5指向baseStr01在常量池中的位置褐澎。

結(jié)果 #6 :因?yàn)閟tr5和str1都指向的都是常量池中的同一個(gè)字符串,所以str1==str5返回true伐蒋。

運(yùn)行代碼段二工三,輸出結(jié)果如下:

image

** 3、代碼段三解析:**

對于代碼段三先鱼,在 JDK 1.6 和 JDK 1.7中的運(yùn)行結(jié)果不同俭正。我們先看一下運(yùn)行結(jié)果,然后再來解釋其原因:

JDK 1.6 下的運(yùn)行結(jié)果:

image

JDK 1.7 下的運(yùn)行結(jié)果:

image

根據(jù)對代碼段一的分析焙畔,應(yīng)該可以很簡單得出 JDK 1.6 的結(jié)果掸读,因?yàn)?str2 和 str1本來就是指向不同的位置,理應(yīng)返回false宏多。

比較奇怪的問題在于JDK 1.7后儿惫,對于第一種情況返回true,但是調(diào)換了一下位置返回的結(jié)果就變成了false伸但。這個(gè)原因主要是從JDK 1.7后肾请,HotSpot 將常量池從永久代移到了元空間,正因?yàn)槿绱烁郑琂DK 1.7 后的intern方法在實(shí)現(xiàn)上發(fā)生了比較大的改變铛铁,JDK 1.7后,intern方法還是會(huì)先去查詢常量池中是否有已經(jīng)存在却妨,如果存在饵逐,則返回常量池中的引用,這一點(diǎn)與之前沒有區(qū)別彪标,區(qū)別在于梳毙,如果在常量池找不到對應(yīng)的字符串,則不會(huì)再將字符串拷貝到常量池捐下,而只是在常量池中生成一個(gè)對原字符串的引用账锹。所以:

結(jié)果 #7:在第一種情況下萌业,因?yàn)槌A砍刂袥]有“str01”這個(gè)字符串,所以會(huì)在常量池中生成一個(gè)對堆中的“str01”的引用奸柬,而在進(jìn)行字面量賦值的時(shí)候生年,常量池中已經(jīng)存在,所以直接返回該引用即可廓奕,因此str1和str2都指向堆中的字符串抱婉,返回true。

結(jié)果 #8:調(diào)換位置以后桌粉,因?yàn)樵谶M(jìn)行字面量賦值(String str1 = "str01")的時(shí)候蒸绩,常量池中不存在,所以str1指向的常量池中的位置铃肯,而str2指向的是堆中的對象患亿,再進(jìn)行intern方法時(shí),對str1和str2已經(jīng)沒有影響了押逼,所以返回false步藕。

三、常見面試題解答

有了對以上的知識(shí)的了解挑格,我們現(xiàn)在再來看常見的面試或筆試題就很簡單了:

Q:String s = new String("xyz")咙冗,創(chuàng)建了幾個(gè)String Object?

A:兩個(gè),常量池中的"xyz"和堆中對象漂彤。

Q:下列程序的輸出結(jié)果:

String s1 = “abc”;
String s2 = “abc”;
System.out.println(s1 == s2);

A:true雾消,均指向常量池中對象。

Q:下列程序的輸出結(jié)果:

String s1 = new String(“abc”);
String s2 = new String(“abc”);
System.out.println(s1 == s2);

A:false挫望,兩個(gè)引用指向堆中的不同對象立润。

Q:下列程序的輸出結(jié)果:

String s1 = “abc”;
String s2 = “a”;
String s3 = “bc”;
String s4 = s2 + s3;
System.out.println(s1 == s4);

A:false,因?yàn)閟2+s3實(shí)際上是使用StringBuilder.append來完成士骤,會(huì)生成不同的對象范删。

Q:下列程序的輸出結(jié)果:

String s1 = “abc”;
final String s2 = “a”;
final String s3 = “bc”;
String s4 = s2 + s3;
System.out.println(s1 == s4);

A:true,因?yàn)閒inal變量在編譯后會(huì)直接替換成對應(yīng)的值拷肌,所以實(shí)際上等于s4="a"+"bc"到旦,而這種情況下巨缘,編譯器會(huì)直接合并為s4="abc"添忘,所以最終s1==s4。

Q:下列程序的輸出結(jié)果:

String s = new String("abc");
String s1 = "abc";
String s2 = new String("abc");

System.out.println(s == s1.intern());
System.out.println(s == s2.intern());
System.out.println(s1 == s2.intern());

A:false若锁,false搁骑,true,具體原因參考第二部分內(nèi)容。

JDK1.8關(guān)于運(yùn)行時(shí)常量池, 字符串常量池的要點(diǎn)

網(wǎng)上關(guān)于jdk 1.8的各種實(shí)驗(yàn), 結(jié)論魚龍混雜 , 很多都相矛盾,網(wǎng)上有的實(shí)驗(yàn)也被后人測試出了不同的結(jié)果

很多都分辨不了真假, 這里記錄一下網(wǎng)絡(luò)上正確的結(jié)論, 歡迎指正!

首先自行區(qū)分運(yùn)行時(shí)常量池Class文件常量池(靜態(tài)常量池)的概念, JVM內(nèi)存模型 ,方法區(qū)與永久代的區(qū)別

JDK1.7之前運(yùn)行時(shí)常量池邏輯包含字符串常量池存放在方法區(qū), 此時(shí)hotspot虛擬機(jī)對方法區(qū)的實(shí)現(xiàn)為永久代

JDK1.7 字符串常量池被從方法區(qū)拿到了堆中, 這里沒有提到運(yùn)行時(shí)常量池,也就是說字符串常量池被單獨(dú)拿到堆,運(yùn)行時(shí)常量池剩下的東西還在方法區(qū), 也就是hotspot中的永久代

JDK1.8 hotspot移除了永久代元空間(****Metaspace)取而代之, 這時(shí)候字符串常量池還在堆, 運(yùn)行時(shí)常量池還在方法區(qū), 只不過方法區(qū)的實(shí)現(xiàn)從永久代變成了元空間(Metaspace)

  • 對于直接做+運(yùn)算的兩個(gè)字符串(字面量)常量仲器,并不會(huì)放入字符串常量池中煤率,而是直接把運(yùn)算后的結(jié)果放入字符串常量池中
    (String s = "abc"+ "def", 會(huì)直接生成“abcdef"字符串常量 而不把 "abc" "def"放進(jìn)常量池)

  • 對于先聲明的字符串字面量常量,會(huì)放入字符串常量池乏冀,但是若使用字面量的引用進(jìn)行運(yùn)算就不會(huì)把運(yùn)算后的結(jié)果放入字符串常量池中了
    (String s = new String("abc") + new String("def"),在構(gòu)造過程中不會(huì)生成“abcdef"字符串常量)

  • 總結(jié)一下就是JVM會(huì)對字符串常量的運(yùn)算進(jìn)行優(yōu)化蝶糯,未聲明的,只放結(jié)果辆沦;已經(jīng)聲明的昼捍,只放聲明

  • 常量池中同時(shí)存在字符串常量和字符串引用。直接賦值和用字符串調(diào)用String構(gòu)造函數(shù)都可能導(dǎo)致常量池中生成字符串常量;而intern()方法會(huì)嘗試將堆中對象的引用放入常量池

  • String str1 = "a";
    String str2 = "b";
    String str4 = str1 + str2;
    //該語句只在堆中生成一個(gè)對象(str4)
    這句被Java編譯器做了優(yōu)化, 實(shí)際上使用StringBuilder實(shí)現(xiàn)的(不在堆里生成str1和str2對象)

  • String str5 = new String("ab");(字符串常量池中不存在"ab"時(shí))在字符換常量池中創(chuàng)建"ab"對象,在堆中生成了一個(gè)對象str5, str5指向堆上new的對象肢扯,而str5內(nèi)部的char value[]則指向常量池中的char value[]
    關(guān)于這個(gè)問題可以參考這篇博客:new String()究竟創(chuàng)建幾個(gè)對象?

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末姿搜,一起剝皮案震驚了整個(gè)濱河市雪营,隨后出現(xiàn)的幾起案子乱顾,更是在濱河造成了極大的恐慌咖杂,老刑警劉巖,帶你破解...
    沈念sama閱讀 206,482評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件蛛株,死亡現(xiàn)場離奇詭異团赁,居然都是意外死亡育拨,警方通過查閱死者的電腦和手機(jī)谨履,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,377評論 2 382
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來熬丧,“玉大人笋粟,你說我怎么就攤上這事∥龊” “怎么了害捕?”我有些...
    開封第一講書人閱讀 152,762評論 0 342
  • 文/不壞的土叔 我叫張陵,是天一觀的道長闷畸。 經(jīng)常有香客問我尝盼,道長,這世上最難降的妖魔是什么佑菩? 我笑而不...
    開封第一講書人閱讀 55,273評論 1 279
  • 正文 為了忘掉前任盾沫,我火速辦了婚禮,結(jié)果婚禮上殿漠,老公的妹妹穿的比我還像新娘赴精。我一直安慰自己,他們只是感情好绞幌,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,289評論 5 373
  • 文/花漫 我一把揭開白布蕾哟。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪谭确。 梳的紋絲不亂的頭發(fā)上帘营,一...
    開封第一講書人閱讀 49,046評論 1 285
  • 那天,我揣著相機(jī)與錄音逐哈,去河邊找鬼仪吧。 笑死,一個(gè)胖子當(dāng)著我的面吹牛鞠眉,可吹牛的內(nèi)容都是我干的薯鼠。 我是一名探鬼主播,決...
    沈念sama閱讀 38,351評論 3 400
  • 文/蒼蘭香墨 我猛地睜開眼械蹋,長吁一口氣:“原來是場噩夢啊……” “哼出皇!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起哗戈,我...
    開封第一講書人閱讀 36,988評論 0 259
  • 序言:老撾萬榮一對情侶失蹤郊艘,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后唯咬,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體纱注,經(jīng)...
    沈念sama閱讀 43,476評論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 35,948評論 2 324
  • 正文 我和宋清朗相戀三年胆胰,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了狞贱。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 38,064評論 1 333
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡蜀涨,死狀恐怖瞎嬉,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情厚柳,我是刑警寧澤氧枣,帶...
    沈念sama閱讀 33,712評論 4 323
  • 正文 年R本政府宣布,位于F島的核電站别垮,受9級特大地震影響便监,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜碳想,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,261評論 3 307
  • 文/蒙蒙 一烧董、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧移袍,春花似錦解藻、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,264評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽啡浊。三九已至,卻和暖如春胶背,著一層夾襖步出監(jiān)牢的瞬間巷嚣,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 31,486評論 1 262
  • 我被黑心中介騙來泰國打工钳吟, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留廷粒,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 45,511評論 2 354
  • 正文 我出身青樓红且,卻偏偏與公主長得像坝茎,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個(gè)殘疾皇子暇番,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,802評論 2 345

推薦閱讀更多精彩內(nèi)容