流程圖詳解 new String("abc") 創(chuàng)建了幾個(gè)字符串對(duì)象
前言
這道題是我之前的面試題文章《Java 基礎(chǔ)高頻面試題(2021年最新版)》里的第10題,今天通過(guò)字節(jié)碼和流程圖來(lái)跟大家詳解一下完整的執(zhí)行過(guò)程。
同時(shí)也會(huì)涉及一些字符串常量池的相關(guān)知識(shí),這塊內(nèi)容網(wǎng)上現(xiàn)在的說(shuō)法有太多錯(cuò)誤了。
本文內(nèi)容有視頻版本,喜歡看視頻的同學(xué)可以直接通過(guò)下面的二維碼觀看。如果你對(duì)文章的內(nèi)容有疑惑,可以先看視頻的對(duì)應(yīng)內(nèi)容,視頻可能講的會(huì)更細(xì)一點(diǎn)。

答案
首先直接說(shuō)答案,一個(gè)比較合理的答案是:一個(gè)或者兩個(gè)字符串對(duì)象,通常這個(gè)也是面試官想要聽(tīng)到的答案。
首先,new string 這邊由于 new 關(guān)鍵字,所以這邊肯定會(huì)在堆中新建一個(gè)字符串對(duì)象。
其次,如果字符串常量池中不存在 jionghui(equals比較)這個(gè)字符串,則會(huì)在字符串常量池中創(chuàng)建一個(gè)字符串對(duì)象。
注意:這邊說(shuō)的在字符串常量池創(chuàng)建對(duì)象,最終對(duì)象還是在堆中創(chuàng)建,字符串常量池只放引用。
例子1:String?str1?=?new?String("jionghui")?
本例子按照字符串常量池中不存在 jionghui 字符串來(lái)說(shuō)。
該代碼編譯后其字節(jié)碼如下:
0 new #2String >3 dup4 ldc #36 invokespecial #4String .: (Ljava/lang/String;)V> ?9?astore_110?return
接下來(lái)我們解釋下這些字節(jié)碼
1)#2、#3、#4
字節(jié)碼中這些帶 # 號(hào)的數(shù)字,是我們常量池里面的符號(hào)引用,這些符號(hào)引用會(huì)在類(lèi)加載的解析階段被解析為直接引用,直接引用可以理解為就是對(duì)象在內(nèi)存中的地址。
這些符號(hào)引用對(duì)應(yīng)的內(nèi)容在后面已經(jīng)給你列出來(lái)了。
#2 這邊對(duì)應(yīng)的是 java.lang.String 的 Class 類(lèi)
#3 對(duì)應(yīng)的 jionghui 字符串
#4 對(duì)應(yīng)的 String 的初始化方法
2)new
new 關(guān)鍵字就是新建對(duì)象的意思,這邊相當(dāng)于會(huì)新建一個(gè) String 對(duì)象,但是此時(shí)還未初始化,是一個(gè)空對(duì)象。同時(shí),這個(gè)字節(jié)碼會(huì)將創(chuàng)建的對(duì)象的引用存放到操作數(shù)棧的棧頂。
執(zhí)行完該指令后的結(jié)構(gòu):

3)dup
復(fù)制的意思,這邊就是復(fù)制一份棧頂?shù)脑亍?/span>
這邊在棧頂復(fù)制一個(gè) String 的引用是因?yàn)楹罄m(xù)調(diào)用 String 的初始化方法會(huì)消耗掉棧里的一個(gè)引用,所以這邊提前復(fù)制一份出來(lái),最后才有引用可以賦值給局部變量表的str1。
執(zhí)行完該指令后的結(jié)構(gòu):

4)ldc
將int、float或String型常量值從常量池中推送至棧頂,這個(gè)地方 ldc 指令會(huì)附帶另外一個(gè)功能:觸發(fā)符號(hào)引用解析為直接引用。
我們上文說(shuō)過(guò)符號(hào)引用會(huì)在解析階段被解析成直接引用,但是有一些特例。字符串對(duì)象就是一個(gè)特例,字符串對(duì)象不會(huì)在解析階段就將符號(hào)引用解析成直接引用,而是等到某個(gè)“合適的時(shí)機(jī)”才去解析,這邊的 ldc 就是這個(gè)時(shí)機(jī)。
PS:下面的例子5會(huì)驗(yàn)證這個(gè)說(shuō)法。
因此 ldc 在這邊會(huì)做兩件事:
1、判斷符號(hào)引用是否已經(jīng)解析成了直接引用,如果沒(méi)有,則會(huì)進(jìn)行解析:判斷 jionghui 字符串是否已經(jīng)在字符串常量池存在,如果存在則將符號(hào)引用解析成字符串常量池的引用;如果不存在,則會(huì)在字符串常量池中創(chuàng)建一個(gè)jionghui 字符串對(duì)象,然后同樣將符號(hào)引用解析成字符串常量池的引用。
2、將對(duì)應(yīng)的字符串常量池推送到棧頂。
執(zhí)行完該指令后的結(jié)構(gòu):

5)invokespecial
調(diào)用超類(lèi)構(gòu)造方法,實(shí)例初始化方法,私有方法。
在這邊用于調(diào)用 String 的初始化方法,我們上面通過(guò) new 關(guān)鍵詞創(chuàng)建的是個(gè)空對(duì)象,還未進(jìn)行初始化。
這邊初始化會(huì)使用到我們棧頂?shù)膬蓚€(gè)元素,一個(gè)元素指向我們要初始化的對(duì)象,另一個(gè)元素指向我們初始化使用的參數(shù)。
這邊初始化完畢后,這個(gè)空字符串對(duì)象會(huì)被初始化成 jionghui 字符串對(duì)象。
執(zhí)行完該指令后的結(jié)構(gòu):

6)astore_1
將棧頂引用元素存到指定本地變量。
這邊最后將棧頂?shù)倪@個(gè)引用存放到本地變量表找那個(gè)的 str1 變量。
執(zhí)行完該指令后的結(jié)構(gòu):

執(zhí)行完畢后,最終如上圖所示。可以看到最終就是創(chuàng)建了兩個(gè)對(duì)象,一個(gè)是是通過(guò)new string 創(chuàng)建出來(lái)的這個(gè)對(duì)象,它的引用被復(fù)賦值給 str1,另外一個(gè)是在常量池里創(chuàng)建的字符串對(duì)象。
例子2:String str2 = "jionghui"
這個(gè)例子就是例子1的簡(jiǎn)版,去掉了 new String 的過(guò)程,其他基本一樣。
執(zhí)行結(jié)束的內(nèi)存結(jié)構(gòu)如下圖所示:

例子3:String str3 = "jiong" + "hui";
該例子在編譯后,這2個(gè)字符串會(huì)被自動(dòng)合并成 jionghui,所以最終跟例子2完全一樣,編譯后的字節(jié)碼都是完全一樣的。
執(zhí)行結(jié)束的內(nèi)存結(jié)構(gòu)如下圖所示:

例子4:String str4 = new String("jiong") + "hui"
核心流程如下:
1)雙引號(hào)修飾的字面量?jiong 和 hui 分別會(huì)在字符串常量池中創(chuàng)建字符串對(duì)象
2)new String 關(guān)鍵字會(huì)再創(chuàng)建一個(gè) jiong 字符串對(duì)象
3)最后這個(gè)字符串拼接,這個(gè)地方不看字節(jié)碼的話很難看出究竟是怎么拼接的,通過(guò)字節(jié)碼一下子就看出來(lái)了,這邊是通過(guò) StringBuilder 來(lái)進(jìn)行字符串的拼接操作,先創(chuàng)建了一個(gè) StringBuilder,然后 append("jiong"),然后 append("hui"),最后執(zhí)行 toString 返回,這邊 toString 底層是通過(guò) new String 方法返回,所以最終這邊拼接也會(huì)創(chuàng)建一個(gè)新的字符串。
字節(jié)碼如下:用到的命令都是上文提過(guò)的。
0 new3 dup4 invokespecial7 new10 dup11 ldc13 invokespecial16 invokevirtual19 ldc21 invokevirtual24 invokevirtual27 astore_128?return
執(zhí)行結(jié)束的內(nèi)存結(jié)構(gòu)如下圖所示:

例子5:intern 測(cè)試1
String str5 = new String("1") + new String("1");str5.intern();String str6 = "11";System.out.println(str5 == str6);
intern:如果字符串常量池中存在當(dāng)前字符串, 則返回常量池中的字符串引用。否則, 將該字符串放入常量池,然后返回該字符串對(duì)象的引用。
intern 在 JDK6 和 JDK7 及之后的版本有些不同,這邊會(huì)簡(jiǎn)單說(shuō)下不同的地方。
JDK7下的核心流程如下:
1)雙引號(hào)修飾的字面量 1?會(huì)在字符串常量池中創(chuàng)建字符串對(duì)象,這邊有2個(gè)字面量 1,但是只會(huì)創(chuàng)建1次,另一個(gè)直接復(fù)用
2)兩個(gè) new String 創(chuàng)建了2個(gè)字符串對(duì)象?1
3)字符串拼接通過(guò) StringBuilder?創(chuàng)建出1個(gè)新的字符串對(duì)象 11,并將引用賦值給 str5
4)str5 調(diào)用 intern 方法,檢查到字符串常量池還沒(méi)有字符串11,則將字符串對(duì)象放入常量池,此時(shí)字符串常量池中的 11 就是 str5 指向的字符串對(duì)象
5)雙引號(hào)修飾的字面量?11?檢查到字符串常量池中已經(jīng)存在字符串 11,則直接使用字符串常量池中的對(duì)象,所以 str6 被賦值為字符串常量池中的對(duì)象引用,也就是 str5的引用
6)輸出結(jié)果為 true
執(zhí)行結(jié)束的內(nèi)存結(jié)構(gòu)如下圖所示:

而 JDK6 下的流程有什么不同呢,主要在于 JDK6 版本還存在永久代的概念,字符串常量池指向的字符串對(duì)象在 JDK6 中是在永久代創(chuàng)建的,JDK7才被移動(dòng)到堆中。
所以當(dāng)執(zhí)行 str5.intern 時(shí),發(fā)現(xiàn)永久代中沒(méi)有字符串11,則會(huì)在永久代創(chuàng)建字符串對(duì)象11,后續(xù)的 str6 也是指向永久代的字符串對(duì)象。所以,此時(shí) str5 和 str6 指向的不同對(duì)象。
因此,JDK6 的輸出結(jié)果為 false。
執(zhí)行結(jié)束的內(nèi)存結(jié)構(gòu)如下圖所示:

驗(yàn)證字符串對(duì)象在運(yùn)行中在解析符號(hào)引用
這個(gè)例子還能驗(yàn)證我們上面說(shuō)的:字符串的符號(hào)引用在運(yùn)行階段才被解析成直接引用的說(shuō)法。
我們假設(shè)字符串的符號(hào)引用也是在類(lèi)加載的解析階段就解析成直接引用了,那么這個(gè)例子的流程如下(JDK7及之后版本):
1)解析階段,雙引號(hào)修飾的字面量 1 和 11 會(huì)在字符串常量池中創(chuàng)建字符串對(duì)象
2)兩個(gè) new String 創(chuàng)建了2個(gè)字符串對(duì)象?1
3)字符串拼接通過(guò) StringBuilder?創(chuàng)建出1個(gè)新的字符串對(duì)象 11,并將引用賦值給 str5
4)str5 調(diào)用 intern 方法,檢查到字符串常量池存在字符串11,則不做任何操作
5)str6 被賦值為字符串常量池中的對(duì)象引用,此時(shí) str6 和 str5 指向的是不同的字符串對(duì)象
6)輸出結(jié)果為 false
本例在 JDK7及之后版本的輸出結(jié)果為 true,驗(yàn)證了我們的說(shuō)法。
字符串常量池中的字符串對(duì)象使用懶加載在 JVM 源碼中是有明確注釋的,同時(shí) R 大也在某論壇上說(shuō)過(guò)。
例子6:intern 測(cè)試2
這個(gè)例子就是將例子5的2和3行代碼調(diào)換了下順序,驗(yàn)證一下 intern 方法的返回值。
String str7 = new String("1") + new String("1");String str8 = "11";String str9 = str7.intern();System.out.println(str7 == str8);System.out.println(str8 == str9);
核心流程如下:
1)雙引號(hào)修飾的字面量 1?會(huì)在字符串常量池中創(chuàng)建字符串對(duì)象,這邊有2個(gè)字面量 1,但是只會(huì)創(chuàng)建1次,另一個(gè)直接復(fù)用
2)兩個(gè) new String 創(chuàng)建了2個(gè)字符串對(duì)象?1
3)字符串拼接通過(guò) StringBuilder?創(chuàng)建出1個(gè)新的字符串對(duì)象 11,并將引用賦值給 str7
3)雙引號(hào)修飾的字面量 11 會(huì)在字符串常量池中創(chuàng)建字符串對(duì)象,并將引用賦值給 str8
4)str7 調(diào)用 intern 方法,檢查到字符串常量池存在字符串11,則不做任何操作,同時(shí)返回字符串常量池的引用,并賦值給 str9,也就是 str8 指向的引用
5)輸出結(jié)果為 false 和 true
執(zhí)行結(jié)束的內(nèi)存結(jié)構(gòu)如下圖所示:

推薦閱讀
最近我將面試:阿里、字節(jié)、美團(tuán)、快手、拼多多等大廠的高頻面試整理出來(lái),并按大廠的標(biāo)準(zhǔn)給出自己的解析。
群里有不少同學(xué)看完拿下了阿里、美團(tuán)等大廠 Offer,希望能助你一臂之力,早日拿下大廠 Offer。
獲取方式:關(guān)注公眾號(hào)回復(fù)【面試】即可領(lǐng)取,更多大廠面試真題解析 PDF 整理中。

