<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          從字符串到常量池,一文看懂String類(lèi)

          共 13479字,需瀏覽 27分鐘

           ·

          2020-09-24 19:56


          本文要點(diǎn)

          • class 文件中的常量池

            • 位置在哪?

            • 用來(lái)干什么的?

          • 字符串常量池

            • 位置在哪?

            • 用來(lái)干什么的?

            • intern 方法分析

          • 運(yùn)行時(shí)常量池

            • 位置在哪?

            • 用來(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)題

          1. 你說(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ì)象
          2. 堆跟常量池到底什么關(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)題?

          1. 這個(gè)常量池在哪里?
          2. 這個(gè)常量池用來(lái)干什么呢?
          3. 這三者有什么關(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)表示

          image-20200615225016604

          對(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_infotag標(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è)流程,主要分為四步

          1. 解析ldc指令的符號(hào)引用(#2

          2. #2對(duì)應(yīng)的常量的引用壓入到操作數(shù)棧頂

          3. 將操作數(shù)棧的元素彈出并存儲(chǔ)到局部變量表中

          4. 執(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è)什么東西呢?我們還是分為兩部分討論

          1. 位置在哪?
          2. 用來(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)注意

          1. 方法區(qū)JVM在內(nèi)存分配時(shí)需要遵守的規(guī)范,是一個(gè)理論,具體的實(shí)現(xiàn)可以因人而異
          2. 永久代hotspotjdk1.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。
          3. 元空間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 大博客

          從上面我們可以知道

          1. 字符串常量池本質(zhì)就是一個(gè)哈希表
          2. 字符串常量池中存儲(chǔ)的是字符串實(shí)例的引用
          3. 字符串常量池在被整個(gè) JVM 共享
          4. 在解析運(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 false
          • jdk7 下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è)階段

          1. 解析常量
          2. 調(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è)字符串,StringBuildertoString方法其實(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ī)范》

          參考資料

          [1]

          深入解析 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

          瀏覽 56
          點(diǎn)贊
          評(píng)論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          評(píng)論
          圖片
          表情
          推薦
          點(diǎn)贊
          評(píng)論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  丁香五月婷婷视频在线入口 | 国产经典三级在线 | 一级黄色录像免费看 | 波多野结衣在线不卡 | 豆花视频成人版视频在线观看 |