從字符串到常量池,一文看懂String類(lèi)
從一道面試題開(kāi)始
看到這個(gè)標(biāo)題,你肯定以為我又要講這道面試題了
//??這行代碼創(chuàng)建了幾個(gè)對(duì)象?
String?s3?=?new?String("1");
是的,沒(méi)錯(cuò),我確實(shí)要從這里開(kāi)始

這道題就算你沒(méi)做過(guò)也肯定看到,總所周知,它創(chuàng)建了兩個(gè)對(duì)象,一個(gè)位于堆上,一個(gè)位于常量池中。
這個(gè)答案粗看起來(lái)是沒(méi)有任何問(wèn)題的,但是仔細(xì)思考確經(jīng)不起推敲。
如果你覺(jué)得我說(shuō)的不對(duì)的話(huà),那么可以思考下面這兩個(gè)問(wèn)題
你說(shuō)它創(chuàng)建了兩個(gè)對(duì)象,那么這兩個(gè)對(duì)象分別是怎樣創(chuàng)建的呢?我們回顧下 Java 創(chuàng)建對(duì)象的方式,一共就這么幾種
你說(shuō)它創(chuàng)建了兩個(gè)對(duì)象,那你告訴我除了 new 出來(lái)那個(gè)對(duì)象外,另外一個(gè)對(duì)象怎么創(chuàng)建出來(lái)的?
使用 new 關(guān)鍵字創(chuàng)建對(duì)象 使用反射創(chuàng)建對(duì)象(包括 Class 類(lèi)的 newInstance方法,以及 Constructor 類(lèi)的newInstance方法)使用 clone 復(fù)制一個(gè)對(duì)象 反序列化得到一個(gè)對(duì)象 堆跟常量池到底什么關(guān)系?不是說(shuō)在
JDK1.7之后(含 1.7 版本)常量池已經(jīng)移到了堆中了嗎?如果說(shuō)常量池本身就位于堆中的話(huà),那么這種一個(gè)對(duì)象在堆中,一個(gè)對(duì)象在常量池的說(shuō)法還準(zhǔn)確嗎?
如果你也產(chǎn)生過(guò)這些疑問(wèn)的話(huà),那么請(qǐng)耐心看完這篇文章!要解釋上面的問(wèn)題首先我們得對(duì)常量池有個(gè)準(zhǔn)確的認(rèn)知。

通常來(lái)說(shuō),我們提到的常量池分為三種
class 文件中的常量池 運(yùn)行時(shí)常量池 字符串常量池
對(duì)于這三種常量池,我們需要搞懂下面幾個(gè)問(wèn)題?
這個(gè)常量池在哪里? 這個(gè)常量池用來(lái)干什么呢? 這三者有什么關(guān)系?
接下來(lái),我們帶著這些問(wèn)題往下看

class文件常量池
位置在哪?
顧名思義,class 文件中的常量池當(dāng)然是位于 class 文件中,而class 文件又是位于磁盤(pán)上。
用來(lái)干什么的?
在學(xué)習(xí) class 文件中的常量池前,我們首選需要對(duì) class 文件的結(jié)構(gòu)有一定了解
Class 文件是一組以 8 個(gè)字節(jié)為基礎(chǔ)單位的二進(jìn)制流,各個(gè)數(shù)據(jù)項(xiàng)目嚴(yán)格按照順序緊湊地排列在文
件之中,中間沒(méi)有添加任何分隔符,這使得整個(gè) Class 文件中存儲(chǔ)的內(nèi)容幾乎全部是程序運(yùn)行的必要數(shù)
據(jù),沒(méi)有空隙存在。
------------《深入理解 Java 虛擬機(jī)》
整個(gè) class 文件的組成可以用下圖來(lái)表示

對(duì)本文而言,我們只關(guān)注其中的常量池部分,常量池可以理解為 class 文件中資源倉(cāng)庫(kù),它是 class 文件結(jié)構(gòu)中與其它項(xiàng)目關(guān)聯(lián)最多的數(shù)據(jù)類(lèi)型,主要用于存放編譯器生成的各種字面量(Literal)和符號(hào)引用(Symbolic References)。字面量就是我們所說(shuō)的常量概念,如文本字符串、被聲明為 final 的常量值等。符號(hào)引用是一組符號(hào)來(lái)描述所引用的目標(biāo),符號(hào)可以是任何形式的字面量,只要使用時(shí)能無(wú)歧義地定位到目標(biāo)即可(它與直接引用區(qū)分一下,直接引用一般是指向方法區(qū)的本地指針,相對(duì)偏移量或是一個(gè)能間接定位到目標(biāo)的句柄)。一般包括下面三類(lèi)常量:
類(lèi)和接口的全限定名 字段的名稱(chēng)和描述符 方法的名稱(chēng)和描述符
現(xiàn)在我們知道了 class 文件中常量池的作用:存放編譯器生成的各種字面量(Literal)和符號(hào)引用(Symbolic References)。很多時(shí)候知道了一個(gè)東西的概念并不能說(shuō)你會(huì)了,對(duì)于程序員而言,如果你說(shuō)你已經(jīng)會(huì)了,那么最好的證明是你能夠通過(guò)代碼將其描述出來(lái),所以,接下來(lái),我想以一種直觀的方式讓大家感受到常量池的存在。通過(guò)分析一段簡(jiǎn)單代碼的字節(jié)碼,讓大家能更好感知常量池的作用。
talk is cheap ,show me code
我們以下面這段代碼為例,通過(guò)javap來(lái)查看 class 文件中的具體內(nèi)容,代碼如下:
/**
?*?@author?程序員DMZ
?*?@Date?Create?in?22:59?2020/6/15
?*?@公眾號(hào)?微信搜索:程序員DMZ
?*/
public?class?Main?{
????public?static?void?main(String[]?args)?{
????????String?name?=?"dmz";
????}
}
進(jìn)入Main.java文件所在目錄,執(zhí)行命令:javac Main.java,那么此時(shí)會(huì)在當(dāng)前目錄下生成對(duì)應(yīng)的Main.class文件。再執(zhí)行命令:javap -v -c Main.class,此時(shí)會(huì)得到如下的解析后的字節(jié)碼信息
public?class?com.dmz.jvm.Main
??minor?version:?0
??major?version:?52
??flags:?ACC_PUBLIC,?ACC_SUPER
//?這里就是常量池了
Constant?pool:
???#1?=?Methodref??????????#4.#20?????????//?java/lang/Object."":()V
???#2?=?String?????????????#21????????????//?dmz
???#3?=?Class??????????????#22????????????//?com/dmz/jvm/Main
???#4?=?Class??????????????#23????????????//?java/lang/Object
???#5?=?Utf8???????????????
???#6?=?Utf8???????????????()V
???#7?=?Utf8???????????????Code
???#8?=?Utf8???????????????LineNumberTable
???#9?=?Utf8???????????????LocalVariableTable
??#10?=?Utf8???????????????this
??#11?=?Utf8???????????????Lcom/dmz/jvm/Main;
??#12?=?Utf8???????????????main
??#13?=?Utf8???????????????([Ljava/lang/String;)V
??#14?=?Utf8???????????????args
??#15?=?Utf8???????????????[Ljava/lang/String;
??#16?=?Utf8???????????????name
??#17?=?Utf8???????????????Ljava/lang/String;
??#18?=?Utf8???????????????SourceFile
??#19?=?Utf8???????????????Main.java
??#20?=?NameAndType????????#5:#6??????????//?"":()V
??#21?=?Utf8???????????????dmz
??#22?=?Utf8???????????????com/dmz/jvm/Main
??#23?=?Utf8???????????????java/lang/Object
?//?下面是方法表
{
??public?com.dmz.jvm.Main();
????descriptor:?()V
????flags:?ACC_PUBLIC
????Code:
??????stack=1,?locals=1,?args_size=1
?????????0:?aload_0
?????????1:?invokespecial?#1??????????????????//?Method?java/lang/Object."":()V
?????????4:?return
??????LineNumberTable:
????????line?7:?0
??????LocalVariableTable:
????????Start??Length??Slot??Name???Signature
????????????0???????5?????0??this???Lcom/dmz/jvm/Main;
??public?static?void?main(java.lang.String[]);
????descriptor:?([Ljava/lang/String;)V
????flags:?ACC_PUBLIC,?ACC_STATIC
????Code:
??????stack=1,?locals=2,?args_size=1
?????????//?可以看到方法表中的指令引用了常量池中的常量,這也是為什么說(shuō)常量池是資源倉(cāng)庫(kù)的原因
?????????//?因?yàn)樗鼤?huì)被class文件中的其它結(jié)構(gòu)引用
?????????0:?ldc???????????#2??????????????????//?String?dmz
?????????2:?astore_1
?????????3:?return
??????LineNumberTable:
????????line?9:?0
????????line?10:?3
??????LocalVariableTable:
????????Start??Length??Slot??Name???Signature
????????????0???????4?????0??args???[Ljava/lang/String;
????????????3???????1?????1??name???Ljava/lang/String;
}
SourceFile:?"Main.java"
在上面的字節(jié)碼中,我們暫且關(guān)注常量池中的內(nèi)容即可。主要看這兩行
#2?=?String?????????????#14???????????//?dmz
#14?=?Utf8???????????????dmz
如果要看懂這兩行代碼,我們需要對(duì)常量池中 String 類(lèi)型常量的結(jié)構(gòu)有一定了解,其結(jié)構(gòu)如下:
| CONSTANT_String_info | tag | 標(biāo)志常量類(lèi)型的標(biāo)簽 |
|---|---|---|
| index | 指向字符串字面量的索引 |
對(duì)應(yīng)到我們上面的字節(jié)碼中,tag=String,index=#14,所以我們可以知道,#2是一個(gè)字面量為#14的字符串類(lèi)型常量。而#14對(duì)應(yīng)的字面量信息(一個(gè)Utf8類(lèi)型的常量)就是dmz。
常量池作為資源倉(cāng)庫(kù),最大的用處在于被 class 文件中的其它結(jié)構(gòu)所引用,這個(gè)時(shí)候我們?cè)賹⒆⒁饬Ψ诺?main 方法上來(lái),對(duì)應(yīng)的就是這三條指令
0:?ldc???????????#2????????????//?String?dmz
2:?astore_1
3:?return
ldc:這個(gè)指令的作用是將對(duì)應(yīng)的常量的引用壓入操作數(shù)棧,在執(zhí)行ldc指令時(shí)會(huì)觸發(fā)對(duì)它的符號(hào)引用進(jìn)行解析,在上面例子中對(duì)應(yīng)的符號(hào)引用就是#2,也就是常量池中的第二個(gè)元素(這里就能看出方法表中就引用了常量池中的資源)
astore_1:將操作數(shù)棧底元素彈出,存儲(chǔ)到局部變量表中的 1 號(hào)元素
return:方法返回值為 void,標(biāo)志方法執(zhí)行完成,將方法對(duì)應(yīng)棧幀從棧中彈出
下面我用畫(huà)圖的方式來(lái)畫(huà)出整個(gè)流程,主要分為四步
解析
ldc指令的符號(hào)引用(#2)將
#2對(duì)應(yīng)的常量的引用壓入到操作數(shù)棧頂將操作數(shù)棧的元素彈出并存儲(chǔ)到局部變量表中
執(zhí)行
return指令,方法執(zhí)行結(jié)束,彈出棧區(qū)該方法對(duì)應(yīng)的棧幀
第一步:

在解析#2這個(gè)符號(hào)引用時(shí),會(huì)先到字符串常量池中查找是否存在對(duì)應(yīng)字符串實(shí)例的引用,如果有的話(huà),那么直接返回這個(gè)字符串實(shí)例的引用,如果沒(méi)有的話(huà),會(huì)創(chuàng)建一個(gè)字符串實(shí)例,那么將其添加到字符串常量池中(實(shí)際上是將其引用放入到一個(gè)哈希表中),之后再返回這個(gè)字符串實(shí)例對(duì)象的引用。
到這里也能回答我們之前提出的那個(gè)問(wèn)題了,一個(gè)對(duì)象是 new 出來(lái)的,另外一個(gè)是在解析常量池的時(shí)候 JVM 自動(dòng)創(chuàng)建的
第二步:

將第一步得到的引用壓入到操作數(shù)棧,此時(shí)這個(gè)字符串實(shí)例同時(shí)被操作數(shù)棧以及字符串常量池引用。
第三步:

操作數(shù)棧中的引用彈出,并賦值給局部變量表中的 1 號(hào)位置元素,到這一步其實(shí)執(zhí)行完了String name = "dmz"這行代碼。此時(shí)局部變量表中儲(chǔ)存著一個(gè)指向堆中字符串實(shí)例的引用,并且這個(gè)字符串實(shí)例同時(shí)也被字符串常量池引用。
第四步:
這一步我就不畫(huà)圖了,就是方法執(zhí)行完成,棧幀彈出,非常簡(jiǎn)單。
在上文中,我多次提到了字符串常量池,它到底是個(gè)什么東西呢?我們還是分為兩部分討論
位置在哪? 用來(lái)干什么的?

字符串常量池
位置在哪?
字符串常量池比較特殊,在JDK1.7之前,其存在于永久代中,到JDK1.7及之后,已經(jīng)中永久代移到了堆中。當(dāng)然,如果你非要說(shuō)永久代也是堆的一部分那我也沒(méi)辦法。
另外還要說(shuō)明一點(diǎn),經(jīng)常有同學(xué)會(huì)將方法區(qū),元空間,永久代(permgen space)的概念混淆。請(qǐng)注意
方法區(qū)是JVM在內(nèi)存分配時(shí)需要遵守的規(guī)范,是一個(gè)理論,具體的實(shí)現(xiàn)可以因人而異永久代是hotspot的jdk1.8以前對(duì)方法區(qū)的實(shí)現(xiàn),使用jdk1.7的老司機(jī)肯定以前經(jīng)常遇到過(guò)java.lang.OutOfMemoryError: PremGen space異常。這里的PermGen space其實(shí)指的就是方法區(qū)。不過(guò)方法區(qū)和PermGen space又有著本質(zhì)的區(qū)別。前者是JVM的規(guī)范,而后者則是JVM規(guī)范的一種實(shí)現(xiàn),并且只有HotSpot才有PermGen space。元空間是jdk1.8對(duì)方法區(qū)的實(shí)現(xiàn),jdk1.8徹底移除了永久代,其實(shí),移除永久代的工作從JDK 1.7就開(kāi)始了。JDK 1.7中,存儲(chǔ)在永久代的部分?jǐn)?shù)據(jù)就已經(jīng)轉(zhuǎn)移到 Java Heap 或者 Native Heap。但永久代仍存在于JDK 1.7中,并沒(méi)有完全移除,譬如符號(hào)引用(Symbols)轉(zhuǎn)移到了 native heap;字面量(interned strings)轉(zhuǎn)移到了 Java heap;類(lèi)的靜態(tài)變量(class statics)轉(zhuǎn)移到了 Java heap。到jdk1.8徹底移除了永久代,將 JDK7 中還剩余的永久代信息全部移到元空間,元空間相比對(duì)永久代最大的差別是,元空間使用的是本地內(nèi)存(Native Memory)。
用來(lái)干什么的?
字符串常量池,顧名思義,肯定就是用來(lái)存儲(chǔ)字符串的嘛,準(zhǔn)確來(lái)說(shuō)存儲(chǔ)的是字符串實(shí)例對(duì)象的引用。我查閱了很多博客、資料,它們都會(huì)說(shuō),字符串常量池中存儲(chǔ)的就是字符串對(duì)象。其實(shí)我們可以類(lèi)比下面這段代碼:
HashSet?persons?=?new?HashSet;
在persons這個(gè)集合中,存儲(chǔ)的是Person對(duì)象還是Person對(duì)象對(duì)應(yīng)的引用呢?
所以,請(qǐng)大聲跟我念三遍
字符串常量池存儲(chǔ)的是字符串實(shí)例對(duì)象的引用!
字符串常量池存儲(chǔ)的是字符串實(shí)例對(duì)象的引用!
字符串常量池存儲(chǔ)的是字符串實(shí)例對(duì)象的引用!
下面我們來(lái)看 R 大博文下評(píng)論的一段話(huà):
簡(jiǎn)單來(lái)說(shuō),HotSpot VM 里 StringTable 是個(gè)哈希表,里面存的是駐留字符串的引用(而不是駐留字符串實(shí)例自身)。也就是說(shuō)某些普通的字符串實(shí)例被這個(gè) StringTable 引用之后就等同被賦予了“駐留字符串”的身份。這個(gè) StringTable 在每個(gè) HotSpot VM 的實(shí)例里只有一份,被所有的類(lèi)共享。類(lèi)的運(yùn)行時(shí)常量池里的 CONSTANT_String 類(lèi)型的常量,經(jīng)過(guò)解析(resolve)之后,同樣存的是字符串的引用;解析的過(guò)程會(huì)去查詢(xún) StringTable,以保證運(yùn)行時(shí)常量池所引用的字符串與 StringTable 所引用的是一致的。
------R 大博客
從上面我們可以知道
字符串常量池本質(zhì)就是一個(gè)哈希表 字符串常量池中存儲(chǔ)的是字符串實(shí)例的引用 字符串常量池在被整個(gè) JVM 共享 在解析運(yùn)行時(shí)常量池中的符號(hào)引用時(shí),會(huì)去查詢(xún)字符串常量池,確保運(yùn)行時(shí)常量池中解析后的直接引用跟字符串常量池中的引用是一致的
為了更好理解上面的內(nèi)容,我們需要去分析 String 中的一個(gè)方法-----intern()
intern 方法分析
/**
?*?Returns?a?canonical?representation?for?the?string?object.
?*?
?*?A?pool?of?strings,?initially?empty,?is?maintained?privately?by?the
?*?class?String.
?*?
?*?When?the?intern?method?is?invoked,?if?the?pool?already?contains?a
?*?string?equal?to?this?String?object?as?determined?by
?*?the?{@link?#equals(Object)}?method,?then?the?string?from?the?pool?is
?*?returned.?Otherwise,?this?String?object?is?added?to?the
?*?pool?and?a?reference?to?this?String?object?is?returned.
?*?
?*?It?follows?that?for?any?two?strings?s?and?t,
?*?s.intern()?==?t.intern()?is?true
?*?if?and?only?if?s.equals(t)?is?true.
?*?
?*?All?literal?strings?and?string-valued?constant?expressions?are
?*?interned.?String?literals?are?defined?in?section?3.10.5?of?the
?*?The?Java??Language?Specification.
?*
?*?@return??a?string?that?has?the?same?contents?as?this?string,?but?is
?*??????????guaranteed?to?be?from?a?pool?of?unique?strings.
?*/
public?native?String?intern();
String#intern方法中看到,這個(gè)方法是一個(gè) native 的方法,但注釋寫(xiě)的非常明了?!叭绻A砍刂写嬖诋?dāng)前字符串, 就會(huì)直接返回當(dāng)前字符串. 如果常量池中沒(méi)有此字符串, 會(huì)將此字符串放入常量池中后, 再返回”。
關(guān)于其詳細(xì)的分析可以參考:美團(tuán):深入解析 String#intern[1]
珠玉在前,所以本文著重就分析下 intern 方法在JDK不同版本下的差異,首先我們要知道引起差異的原因是因?yàn)?code style>JDK1.7及之后將字符串常量池從永久代挪到了堆中。
我這里就以美團(tuán)文章中的示例代碼來(lái)進(jìn)行分析,代碼如下:
public?static?void?main(String[]?args)?{
????String?s?=?new?String("1");
????s.intern();
????String?s2?=?"1";
????System.out.println(s?==?s2);
????String?s3?=?new?String("1")?+?new?String("1");
????s3.intern();
????String?s4?=?"11";
????System.out.println(s3?==?s4);
}
打印結(jié)果是
jdk6 下 false falsejdk7 下 false true
在美團(tuán)的文章中已經(jīng)對(duì)這個(gè)結(jié)果做了詳細(xì)的解釋?zhuān)酉聛?lái)我就用我的圖解方式再分析一波這個(gè)過(guò)程
jdk6 執(zhí)行流程
第一步:執(zhí)行String s = new String("1"),要清楚這行代碼的執(zhí)行過(guò)程,我們還是得從字節(jié)碼入手,這行代碼對(duì)應(yīng)的字節(jié)碼如下:
??public?static?void?main(java.lang.String[]);
????Code:
???????0:?new???????????#2??????????????????//?class?java/lang/String
???????3:?dup
???????4:?ldc???????????#3??????????????????//?String?1
???????6:?invokespecial?#4??????????????????//?Method?java/lang/String."":(Ljava/lang/String;)V
???????9:?astore_1
??????10:?return
new :創(chuàng)建了一個(gè)類(lèi)的實(shí)例(還沒(méi)有調(diào)用構(gòu)造器函數(shù)),并將其引用壓入操作數(shù)棧頂
dup:復(fù)制棧頂數(shù)值并將復(fù)制值壓入棧頂,這是因?yàn)?code style>invokespecial跟astore_1各需要消耗一個(gè)引用
ldc:解析常量池符號(hào)引用,將實(shí)際的直接引用壓入操作數(shù)棧頂
invokespecial:彈出此時(shí)棧頂?shù)某A恳眉皩?duì)象引用,執(zhí)行invokespecial指令,調(diào)用構(gòu)造函數(shù)
astore_1:將此時(shí)操作數(shù)棧頂?shù)脑貜棾?,賦值給局部變量表中 1 號(hào)元素(0 號(hào)元素存的是 main 函數(shù)的參數(shù))
我們可以將上面整個(gè)過(guò)程分為兩個(gè)階段
解析常量 調(diào)用構(gòu)造函數(shù)創(chuàng)建對(duì)象并返回引用
在解析常量的過(guò)程中,因?yàn)樵撟址A渴堑谝淮谓馕?,所以?huì)先在永久代中創(chuàng)建一個(gè)字符串實(shí)例對(duì)象,并將其引用添加到字符串常量池中。此時(shí)內(nèi)存狀態(tài)如下:

當(dāng)真正通過(guò) new 方式創(chuàng)建對(duì)象完成后,對(duì)應(yīng)的內(nèi)存狀態(tài)如下,因?yàn)樵诜治?code style>class文件中的常量池的時(shí)候已經(jīng)對(duì)棧區(qū)做了詳細(xì)的分析,所以這里就省略一些細(xì)節(jié)了,在執(zhí)行完這行代碼后,棧區(qū)存在一個(gè)引用,指向 了堆區(qū)的一個(gè)字符串實(shí)例內(nèi)存狀態(tài)對(duì)應(yīng)如下:

第二步:緊接著,我們調(diào)用了 s 的 intern 方法,對(duì)應(yīng)代碼就是s.intern()
當(dāng) intern 方法執(zhí)行時(shí),因?yàn)榇藭r(shí)字符串常量池中已經(jīng)存在了一個(gè)字面量信息跟 s 相同的字符串的引用,所以此時(shí)內(nèi)存狀態(tài)不會(huì)發(fā)生任何改變。
第三步:執(zhí)行String s2 = "1",此時(shí)因?yàn)槌A砍刂幸呀?jīng)存在了字面量 1 的對(duì)應(yīng)字符串實(shí)例的引用,所以,這里就直接返回了這個(gè)引用并且賦值給了局部變量 s2。對(duì)應(yīng)的內(nèi)存狀態(tài)如下:

到這里就很清晰了,s 跟 s2 指向兩個(gè)不同的對(duì)象,所以 s==s2 肯定是 false 嘛~
如果看過(guò)美團(tuán)那篇文章的同學(xué)可能會(huì)有些疑惑,我在圖中對(duì)常量池的描述跟美團(tuán)文章圖中略有差異,在美團(tuán)那篇文章中,直接將具體的字符串實(shí)例放到了字符串常量池中,而在我上面的圖中,字符串常量池存的永遠(yuǎn)時(shí)引用,它的圖是這樣畫(huà)的

就我查閱的資料而言,我個(gè)人不贊同這種說(shuō)法,常量池中應(yīng)該保存的僅僅是引用。關(guān)于這個(gè)問(wèn)題,我已經(jīng)向美團(tuán)的團(tuán)隊(duì)進(jìn)行了留言,也請(qǐng)大佬出來(lái)糾錯(cuò)!
接著我們分析 s3 跟 s4,對(duì)應(yīng)的就是這幾行代碼:
String?s3?=?new?String("1")?+?new?String("1");
s3.intern();
String?s4?=?"11";
System.out.println(s3?==?s4);
我們一行行分析,看看執(zhí)行完后,內(nèi)存的狀態(tài)是什么樣的
第一步:String s3 = new String("1") + new String("1"),執(zhí)行完成后,堆區(qū)多了兩個(gè)匿名對(duì)象,這個(gè)我們不用多關(guān)注,另外堆區(qū)還多了一個(gè)字面量為 11 的字符串實(shí)例,并且棧中存在一個(gè)引用指向這個(gè)實(shí)例
實(shí)際上上圖中還少了一個(gè)匿名的StringBuilder的對(duì)象,這是因?yàn)楫?dāng)我們?cè)谶M(jìn)行字符串拼接時(shí),編譯器默認(rèn)會(huì)創(chuàng)建一個(gè)StringBuilder對(duì)象并調(diào)用其append方法來(lái)進(jìn)行拼接,最后再調(diào)用其toString方法來(lái)轉(zhuǎn)換成一個(gè)字符串,StringBuilder的toString方法其實(shí)就是 new 一個(gè)字符串
public?String?toString()?{
????//?Create?a?copy,?don't?share?the?array
????return?new?String(value,?0,?count);
}
這也是為什么在圖中會(huì)說(shuō)在堆上多了一個(gè)字面量為 11 的字符串實(shí)例的原因,因?yàn)閷?shí)際上就是 new 出來(lái)的嘛!
第二步:s3.intern()
調(diào)用intern方法后,因?yàn)樽址A砍刂心壳皼](méi)有 11 這個(gè)字面量對(duì)應(yīng)的字符串實(shí)例的應(yīng)用,所以 JVM 會(huì)先從堆區(qū)復(fù)制一個(gè)字符串實(shí)例到永久代中,再將其引用添加到字符串常量池中,最終的內(nèi)存狀態(tài)就如下所示

第三步:String s4 = "11"
這應(yīng)該沒(méi)啥好說(shuō)的了吧,常量池中有了,直接指向?qū)?yīng)的字符串實(shí)例

到這里可以發(fā)現(xiàn),s3 跟 s4 指向的根本就是兩個(gè)不同的對(duì)象,所以也返回 false
jdk7 執(zhí)行流程
在 jdk1.7 中,s 跟 s2 的執(zhí)行結(jié)果還是一樣的,這是因?yàn)?String s = new String("1")這行代碼本身就創(chuàng)建了兩個(gè)字符串對(duì)象,一個(gè)屬于被常量池引用的駐留字符串,而另外一個(gè)只是堆上的一個(gè)普通字符串對(duì)象。跟 1.6 的區(qū)別在于,1.7 中的駐留字符串位于堆上,而 1.6 中的位于方法區(qū)中,但是本質(zhì)上它們還是兩個(gè)不同的對(duì)象,在下面代碼執(zhí)行完后
????String?s?=?new?String("1");
????s.intern();
????String?s2?=?"1";
????System.out.println(s?==?s2);
內(nèi)存狀態(tài)為:

但是對(duì)于 s3 跟 s4 確不同了,因?yàn)樵?jdk1.7 中不會(huì)再去復(fù)制字符串實(shí)例了,在 intern 方法執(zhí)行時(shí)在發(fā)現(xiàn)堆上有對(duì)應(yīng)的對(duì)象之后,直接將這個(gè)對(duì)應(yīng)的引用添加到字符串常量池中,所以代碼執(zhí)行完,內(nèi)存狀態(tài)對(duì)應(yīng)如下:

看到了吧,s3 跟 s4 指向的同一個(gè)對(duì)象,這是因?yàn)?intern 方法執(zhí)行時(shí),直接 s3 這個(gè)引用復(fù)制到了常量池,之后執(zhí)行String s4= "11"的時(shí)候,直接再將常量池中的引用復(fù)制給了 s4,所以 s3==s4 肯定為 true 啦。
在理解了它們之間的差異之后,我們?cè)賮?lái)思考一個(gè)問(wèn)題,假設(shè)我現(xiàn)在將代碼改成這個(gè)樣子,那么運(yùn)行結(jié)果是什么樣的呢?
public?static?void?main(String[]?args)?{
????String?s?=?new?String("1");
????String?sintern?=?s.intern();
????String?s2?=?"1";
????System.out.println(sintern?==?s2);
????String?s3?=?new?String("1")?+?new?String("1");
????String?s3intern?=?s3.intern();
????String?s4?=?"11";
????System.out.println(s3intern?==?s4);
}
上面這段代碼運(yùn)行起來(lái)結(jié)果會(huì)有差異嗎?大家可以自行思考~
在我們對(duì)字符串常量池有了一定理解之后會(huì)發(fā)現(xiàn),其實(shí)通過(guò)String name = "dmz"這行代碼申明一個(gè)字符串,實(shí)際的執(zhí)行邏輯就像下面這段偽代碼所示
/**
??*?這段代碼邏輯類(lèi)比于
??*?String?s?=?"字面量";這種方式申明一個(gè)字符串
??*?其中字面量就是在""中的值
??*
??*/
public?String?declareString(字面量)?{
????String?s;
????//?這是一個(gè)偽方法,標(biāo)明會(huì)根據(jù)字面量的值到字符串值中查找是否存在對(duì)應(yīng)String實(shí)例的引用
????s?=?findInStringTable(字面量);
????//?說(shuō)明字符串池中已經(jīng)存在了這個(gè)引用,那么直接返回
????if?(s?!=?null)?{
????????return?s;
????}
????//?不存在這個(gè)引用,需要新建一個(gè)字符串實(shí)例,然后調(diào)用其intern方法將其拘留到字符串池中,
????//?最后返回這個(gè)新建字符串的引用
????s?=?new?String(字面量);
????//?調(diào)用intern方法,將創(chuàng)建好的字符串放入到StringTable中,
????//?類(lèi)似就是調(diào)用StringTable.add(s)這也的一個(gè)偽方法
????s.intern();
????return?s;
}
按照這個(gè)邏輯,我們將我們將上面思考題中的所有字面量進(jìn)行替換,會(huì)發(fā)現(xiàn)不管在哪個(gè)版本中結(jié)果都應(yīng)該返回 true。

運(yùn)行時(shí)常量池
位置在哪?
位于方法區(qū)中,1.6 在永久代,1.7 在元空間中,永久代跟元空間都是對(duì)方法區(qū)的實(shí)現(xiàn)
用來(lái)干什么?
jvm 在執(zhí)行某個(gè)類(lèi)的時(shí)候,必須經(jīng)過(guò)加載、連接、初始化,而連接又包括驗(yàn)證、準(zhǔn)備、解析三個(gè)階段。而當(dāng)類(lèi)加載到內(nèi)存中后,jvm 就會(huì)將 class 常量池中的內(nèi)容存放到運(yùn)行時(shí)常量池中,由此可知,運(yùn)行時(shí)常量池也是每個(gè)類(lèi)都有一個(gè)。在上面我也說(shuō)了,class 常量池中存的是字面量和符號(hào)引用,也就是說(shuō)他們存的并不是對(duì)象的實(shí)例,而是對(duì)象的符號(hào)引用值。而經(jīng)過(guò)解析(resolve)之后,也就是把符號(hào)引用替換為直接引用,解析的過(guò)程會(huì)去查詢(xún)?nèi)肿址?,也就是我們上面所說(shuō)的StringTable,以保證運(yùn)行時(shí)常量池所引用的字符串與全局字符串池中所引用的是一致的。
所以簡(jiǎn)單來(lái)說(shuō),運(yùn)行時(shí)常量池就是用來(lái)存放 class 常量池中的內(nèi)容的。

總結(jié)
我們將三者進(jìn)行一個(gè)比較

以一道測(cè)試題結(jié)束
//?環(huán)境1.7及以上
public?class?Clazz?{
????public?static?void?main(String[]?args)?{
????????String?s1?=?new?StringBuilder().append("ja").append("va1").toString();
????????String?s2?=?s1.intern();
????????System.out.println(s1==s2);
????????String?s5?=?"dmz";
????????String?s3?=?new?StringBuilder().append("d").append("mz").toString();
????????String?s4?=?s3.intern();
????????System.out.println(s3?==?s4);
????????String?s7?=?new?StringBuilder().append("s").append("pring").toString();
????????String?s8?=?s7.intern();
????????String?s6?=?"spring";
????????System.out.println(s7?==?s8);
????}
}
答案是 true,false,true。大家可以仔細(xì)思考為什么,如有疑惑可以給我留言,或者進(jìn)群交流!
我叫 DMZ,一個(gè)在學(xué)習(xí)路上匍匐前行的小菜鳥(niǎo)!
參考文章:
R 大博文:請(qǐng)別再拿“String s = new String("xyz");創(chuàng)建了多少個(gè) String 實(shí)例”來(lái)面試了吧
R 大知乎回答:JVM 常量池中存儲(chǔ)的是對(duì)象還是引用呢?[2]
Java 中幾種常量池的區(qū)分[3]
方法區(qū),永久代和元空間[4]
美團(tuán):深入解析 String#intern[5]
參考書(shū)籍:
《深入理解 Java 虛擬機(jī)》第二版
《深入理解 Java 虛擬機(jī)》第三版
《Java 虛擬機(jī)規(guī)范》
參考資料
深入解析 String#intern: https://tech.meituan.com/2014/03/06/in-depth-understanding-string-intern.html
[2]JVM 常量池中存儲(chǔ)的是對(duì)象還是引用呢?: https://www.zhihu.com/question/57109429/answer/151717241
[3]Java 中幾種常量池的區(qū)分: http://tangxman.github.io/2015/07/27/the-difference-of-java-string-pool/
[4]方法區(qū),永久代和元空間: https://www.jianshu.com/p/1b61feb2e336
[5]深入解析 String#intern: https://tech.meituan.com/2014/03/06/in-depth-understanding-string-intern.html
