<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.intern()還有這么深的奧秘

          共 8699字,需瀏覽 18分鐘

           ·

          2021-07-29 23:31

          點(diǎn)擊上方 Java學(xué)習(xí)之道,選擇 設(shè)為星標(biāo)

          每天18:30點(diǎn),干貨準(zhǔn)時(shí)奉上!

          來(lái)源: blog.csdn.net/gcoder_/article/details/106644312
          作者: GuoMell

          Part1背景

          在 JAVA 語(yǔ)言中有8中基本類(lèi)型和一種比較特殊的類(lèi)型String。這些類(lèi)型為了使他們?cè)谶\(yùn)行過(guò)程中速度更快,更節(jié)省內(nèi)存,都提供了一種常量池的概念。常量池就類(lèi)似一個(gè)JAVA系統(tǒng)級(jí)別提供的緩存。

          8種基本類(lèi)型的常量池都是系統(tǒng)協(xié)調(diào)的,String類(lèi)型的常量池比較特殊。它的主要使用方法有兩種:

          • 直接使用雙引號(hào)聲明出來(lái)的String對(duì)象會(huì)直接存儲(chǔ)在常量池中。
          • 如果不是用雙引號(hào)聲明的String對(duì)象,可以使用String提供的intern方法。intern 方法會(huì)從字符串常量池中查詢(xún)當(dāng)前字符串是否存在,若不存在就會(huì)將當(dāng)前字符串放入常量池中

          Part2常量池是什么?

          JVM常量池主要分為Class文件常量池、運(yùn)行時(shí)常量池,全局字符串常量池,以及基本類(lèi)型包裝類(lèi)對(duì)象常量池

          方法區(qū)

          • 方法區(qū)的作用是存儲(chǔ)Java類(lèi)的結(jié)構(gòu)信息,當(dāng)創(chuàng)建對(duì)象后,對(duì)象的類(lèi)型信息存儲(chǔ)在方法區(qū)中,實(shí)例數(shù)據(jù)存放在中。類(lèi)型信息是定義在Java代碼中的常量、靜態(tài)變量、以及類(lèi)中聲明的各種方法,方法字段等;實(shí)例數(shù)據(jù)則是在Java中創(chuàng)建的對(duì)象實(shí)例以及他們的值。
          • 該區(qū)域進(jìn)行內(nèi)存回收的主要目的是對(duì)常量池的回收和對(duì)內(nèi)存數(shù)據(jù)的卸載;一般說(shuō)這個(gè)區(qū)域的內(nèi)存回收率比起Java堆低得多。

          Class文件常量池

          • class文件是一組以字節(jié)為單位的二進(jìn)制數(shù)據(jù)流,在Java代碼的編譯期間,我們編寫(xiě)的Java文件就被編譯為.class文件格式的二進(jìn)制數(shù)據(jù)存放在磁盤(pán)中,其中就包括class文件常量池。
          • class文件常量池主要存放兩大常量:字面量和符號(hào)引用。
          • 字面量:字面量接近java語(yǔ)言層面的常量概念
            • 文本字符串,也就是我們經(jīng)常申明的:public String s = "abc";中的"abc"
            • 用final修飾的成員變量,包括靜態(tài)變量、實(shí)例變量和局部變量:public final static int f = 0x101;,final int temp = 3;
            • 而對(duì)于基本類(lèi)型數(shù)據(jù)(甚至是方法中的局部變量),如int value = 1常量池中只保留了他的的字段描述符int和字段的名稱(chēng)value,他們的字面量不會(huì)存在于常量池。
          • 符號(hào)引用:符號(hào)引用主要設(shè)涉及編譯原理方面的概念
            • 類(lèi)和接口的全限定名,也就是java/lang/String;這樣,將類(lèi)名中原來(lái)的".“替換為”/"得到的,主要用于在運(yùn)行時(shí)解析得到類(lèi)的直接引用
            • 字段的名稱(chēng)和描述符,字段也就是類(lèi)或者接口中聲明的變量,包括類(lèi)級(jí)別變量和實(shí)例級(jí)的變量
            • 方法中的名稱(chēng)和描述符,也即參數(shù)類(lèi)型+返回值附上原文出處鏈接及本聲明。

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

          當(dāng)Java文件被編譯成class文件之后,會(huì)生成上面的class文件常量池,JVM在執(zhí)行某個(gè)類(lèi)的時(shí)候,必須經(jīng)過(guò)加載、鏈接(驗(yàn)證、準(zhǔn)備、解析)、初始化的步驟,運(yùn)行時(shí)常量池則是在JVM將類(lèi)加載到內(nèi)存后,就會(huì)將class常量池中的內(nèi)容存放到運(yùn)行時(shí)常量池中,也就是class常量池被加載到內(nèi)存之后的版本,是方法區(qū)的一部分。在解析階段,會(huì)把符號(hào)引用替換為直接引用,解析的過(guò)程會(huì)去查詢(xún)字符串常量池,也就StringTable,以保證運(yùn)行時(shí)常量池所引用的字符串與字符串常量池中是一致的。

          運(yùn)行時(shí)常量池相對(duì)于class常量池一大特征就是具有動(dòng)態(tài)性,Java規(guī)范并不要求常量只能在運(yùn)行時(shí)才產(chǎn)生,也就是說(shuō)運(yùn)行時(shí)常量池的內(nèi)容并不全部來(lái)自class常量池,在運(yùn)行時(shí)可以通過(guò)代碼生成常量并將其放入運(yùn)行時(shí)常量池中,這種特性被用的最多的就是String.intern()。

          字符串常量池

          在JDK6.0及之前版本,字符串常量池存放在方法區(qū)中,在JDK7.0版本以后,字符串常量池被移到了堆中了。至于為什么移到堆內(nèi),大概是由于方法區(qū)的內(nèi)存空間太小了。在HotSpot VM里實(shí)現(xiàn)的string pool功能的是一個(gè)StringTable類(lèi),它是一個(gè)Hash表,默認(rèn)值大小長(zhǎng)度是1009;這個(gè)StringTable在每個(gè)HotSpot VM的實(shí)例只有一份,被所有的類(lèi)共享。字符串常量由一個(gè)一個(gè)字符組成,放在了StringTable上。

          在JDK6.0中,StringTable的長(zhǎng)度是固定的,長(zhǎng)度就是1009,因此如果放入String Pool中的String非常多,就會(huì)造成hash沖突,導(dǎo)致鏈表過(guò)長(zhǎng),當(dāng)調(diào)用String#intern()時(shí)會(huì)需要到鏈表上一個(gè)一個(gè)找,從而導(dǎo)致性能大幅度下降;在JDK7.0中,StringTable的長(zhǎng)度可以通過(guò)參數(shù)指定。

          字符串常量池設(shè)計(jì)思想:

          • 字符串的分配,和其他的對(duì)象分配一樣,耗費(fèi)高昂的時(shí)間與空間代價(jià),作為最基礎(chǔ)的數(shù)據(jù)類(lèi)型,大量頻繁的創(chuàng)建字符串,極大程度地影響程序的性能
          • JVM為了提高性能和減少內(nèi)存開(kāi)銷(xiāo),在實(shí)例化字符串常量的時(shí)候進(jìn)行了一些優(yōu)化
            • 為字符串開(kāi)辟一個(gè)字符串常量池,類(lèi)似于緩存區(qū)
            • 創(chuàng)建字符串常量時(shí),首先查看字符串常量池是否存在該字符串
            • 存在該字符串,返回引用實(shí)例,不存在,實(shí)例化該字符串并放入池中
          • 實(shí)現(xiàn)的基礎(chǔ)
            • 實(shí)現(xiàn)該優(yōu)化的基礎(chǔ)是因?yàn)樽址遣豢勺兊?,可以不用?dān)心數(shù)據(jù)沖突進(jìn)行共享
            • 運(yùn)行時(shí)實(shí)例創(chuàng)建的全局字符串常量池中有一個(gè)表,總是為池中每個(gè)唯一的字符串對(duì)象維護(hù)一個(gè)引用,這就意味著它們一直引用著字符串常量池中的對(duì)象,所以,在常量池中的這些字符串不會(huì)被垃圾收集器回收

          Part3String.intern()與字符串常量池

          /** 
           * Returns a canonical representation for the string object. 
           * <p> 
           * A pool of strings, initially empty, is maintained privately by the 
           * class <code>String</code>. 
           * <p> 
           * When the intern method is invoked, if the pool already contains a 
           * string equal to this <code>String</code> object as determined by 
           * the {@link #equals(Object)} method, then the string from the pool is 
           * returned. Otherwise, this <code>String</code> object is added to the 
           * pool and a reference to this <code>String</code> object is returned. 
           * <p> 
           * It follows that for any two strings <code>s</code> and <code>t</code>, 
           * <code>s.intern()&nbsp;==&nbsp;t.intern()</code> is <code>true</code> 
           * if and only if <code>s.equals(t)</code> is <code>true</code>. 
           * <p> 
           * All literal strings and string-valued constant expressions are 
           * interned. String literals are defined in section 3.10.5 of the 
           * <cite>The Java&trade; Language Specification</cite>. 
           * 
           * @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();

          字符串常量池的位置也是隨著jdk版本的不同而位置不同。在jdk6中,常量池的位置在永久代(方法區(qū))中,此時(shí)常量池中存儲(chǔ)的是對(duì)象。在jdk7中,常量池的位置在堆中,此時(shí),常量池存儲(chǔ)的就是引用了。在jdk8中,永久代(方法區(qū))被元空間取代了。這里就引出了一個(gè)很常見(jiàn)很經(jīng)典的問(wèn)題,看下面這段代碼。

          @Test
          public void test(){
              String s = new String("2");
              s.intern();
              String s2 = "2";
              System.out.println(s == s2);


              String s3 = new String("3") + new String("3");
              s3.intern();
              String s4 = "33";
              System.out.println(s3 == s4);
          }

          jdk6
          false
          false

          jdk7
          false
          true

          這段代碼在jdk6中輸出是false false,但是在jdk7中輸出的是false true。我們通過(guò)圖來(lái)一行行解釋。

          JDK1.6

          • String s = new String("2");創(chuàng)建了兩個(gè)對(duì)象,一個(gè)在堆中的StringObject對(duì)象,一個(gè)是在常量池中的“2”對(duì)象。

          • s.intern();在常量池中尋找與s變量?jī)?nèi)容相同的對(duì)象,發(fā)現(xiàn)已經(jīng)存在內(nèi)容相同對(duì)象“2”,返回對(duì)象2的地址。

          • String s2 = "2";使用字面量創(chuàng)建,在常量池尋找是否有相同內(nèi)容的對(duì)象,發(fā)現(xiàn)有,返回對(duì)象"2"的地址。

          • System.out.println(s == s2);從上面可以分析出,s變量和s2變量地址指向的是不同的對(duì)象,所以返回false

          • String s3 = new String("3") + new String("3");創(chuàng)建了兩個(gè)對(duì)象,一個(gè)在堆中的StringObject對(duì)象,一個(gè)是在常量池中的“3”對(duì)象。中間還有2個(gè)匿名的new String(“3”)我們不去討論它們。

          • s3.intern();在常量池中尋找與s3變量?jī)?nèi)容相同的對(duì)象,沒(méi)有發(fā)現(xiàn)“33”對(duì)象,在常量池中創(chuàng)建“33”對(duì)象,返回“33”對(duì)象的地址。

          • String s4 = "33";使用字面量創(chuàng)建,在常量池尋找是否有相同內(nèi)容的對(duì)象,發(fā)現(xiàn)有,返回對(duì)象"33"的地址。

          • System.out.println(s3 == s4);從上面可以分析出,s3變量和s4變量地址指向的是不同的對(duì)象,所以返回false

          JDK1.7

          • String s = new String("2");創(chuàng)建了兩個(gè)對(duì)象,一個(gè)在堆中的StringObject對(duì)象,一個(gè)是在堆中的“2”對(duì)象,并在常量池中保存“2”對(duì)象的引用地址。

          • s.intern();在常量池中尋找與s變量?jī)?nèi)容相同的對(duì)象,發(fā)現(xiàn)已經(jīng)存在內(nèi)容相同對(duì)象“2”,返回對(duì)象“2”的引用地址。

          • String s2 = "2";使用字面量創(chuàng)建,在常量池尋找是否有相同內(nèi)容的對(duì)象,發(fā)現(xiàn)有,返回對(duì)象“2”的引用地址。

          • System.out.println(s == s2);從上面可以分析出,s變量和s2變量地址指向的是不同的對(duì)象,所以返回false

          • String s3 = new String("3") + new String("3");創(chuàng)建了兩個(gè)對(duì)象,一個(gè)在堆中的StringObject對(duì)象,一個(gè)是在堆中的“3”對(duì)象,并在常量池中保存“3”對(duì)象的引用地址。中間還有2個(gè)匿名的new String(“3”)我們不去討論它們。

          • s3.intern();在常量池中尋找與s3變量?jī)?nèi)容相同的對(duì)象,沒(méi)有發(fā)現(xiàn)“33”對(duì)象,將s3對(duì)應(yīng)的StringObject對(duì)象的地址保存到常量池中,返回StringObject對(duì)象的地址。

          • String s4 = "33";使用字面量創(chuàng)建,在常量池尋找是否有相同內(nèi)容的對(duì)象,發(fā)現(xiàn)有,返回其地址,也就是StringObject對(duì)象的引用地址。

          • System.out.println(s3 == s4);從上面可以分析出,s3變量和s4變量地址指向的是相同的對(duì)象,所以返回`true。

          Part4String.intern()的應(yīng)用

          在大量字符串讀取賦值的情況下,使用String.intern()會(huì)大大的節(jié)省內(nèi)存空間。

          static final int MAX = 1000 * 10000;
          static final String[] arr = new String[MAX];

          public static void main(String[] args) throws Exception {
              Integer[] DB_DATA = new Integer[10];
              Random random = new Random(10 * 10000);
              for (int i = 0; i < DB_DATA.length; i++) {
                  DB_DATA[i] = random.nextInt();
              }
           long t = System.currentTimeMillis();
              for (int i = 0; i < MAX; i++) {
                  //arr[i] = new String(String.valueOf(DB_DATA[i % DB_DATA.length]));
                   arr[i] = new String(String.valueOf(DB_DATA[i % DB_DATA.length])).intern();
              }

           System.out.println((System.currentTimeMillis() - t) + "ms");
              System.gc();
          }

          運(yùn)行的參數(shù)是:-Xmx2g -Xms2g -Xmn1500M 上述代碼是一個(gè)演示代碼,其中有兩條語(yǔ)句不一樣,一條是使用 intern,一條是未使用 intern。發(fā)現(xiàn)不使用 intern 的代碼生成了1000w 個(gè)字符串,占用了大約640m 空間。使用了 intern 的代碼生成了1345個(gè)字符串,占用總空間 133k 左右。其實(shí)通過(guò)觀察程序中只是用到了10個(gè)字符串,所以準(zhǔn)確計(jì)算后應(yīng)該是正好相差100w 倍。雖然例子有些極端,但確實(shí)能準(zhǔn)確反應(yīng)出 intern 使用后產(chǎn)生的巨大空間節(jié)省。

          利用String的不變性,String.intern()方法本質(zhì)就是維持了一個(gè)String的常量池,而且池里的String應(yīng)該都是唯一的。

          這樣,我們便可以利用這種唯一性,來(lái)做一些文章了。我們可以利用池里String的對(duì)象來(lái)做鎖,實(shí)現(xiàn)對(duì)資源的控制。比如一個(gè)城市的某種資源同一時(shí)間只能一個(gè)線(xiàn)程訪(fǎng)問(wèn),那就可以把城市名的String對(duì)象作為鎖,放到常量池中去,同一時(shí)間只能一個(gè)線(xiàn)程獲得。

          不當(dāng)?shù)氖褂茫篺astjson 中對(duì)所有的 json 的 key 使用了 intern 方法,緩存到了字符串常量池中

          這樣每次讀取的時(shí)候就會(huì)非??欤蟠鬁p少時(shí)間和空間,而且 json 的 key 通常都是不變的。但是這個(gè)地方?jīng)]有考慮到大量的 json key 如果是變化的,那就會(huì)給字符串常量池帶來(lái)很大的負(fù)擔(dān)。

          -- END --

           | 更多精彩文章 -



          加我微信,交個(gè)朋友
          長(zhǎng)按/掃碼添加↑↑↑

          瀏覽 43
          點(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>
                  A片免费看 | 韩国黄色一级视频 | 99久久久无码国产精品免费麻豆 | 亚洲成人一二三区 | 欧美毛片蜜芽免费视频 |