首頁技術文章正文

Java培訓:String類的底層原理和版本演變

更新時間:2022-08-25 來源:黑馬程序員 瀏覽量:

  1 String類的底層演變

 ?。?) JDK8以及之前版本 

1661409005970_1.jpg

       (2)JDK9以及之后版本 

1661409021085_2.jpg

  ```java

  JDK8的字符串存儲在char類型的數(shù)組里面,在java中,一個char類型占兩個字節(jié)。但是很多時候,一個字符只需要一個字節(jié)就可存儲,比如各種字母什么的,兩個字節(jié)存儲勢必會浪費空間,JDK9的一個優(yōu)化就在這,內(nèi)存的優(yōu)化,所以JDK9之后字符串改成byte類型數(shù)組進行存儲。

  private final byte coder;

  在JDK9的String類中,新增了一個屬性coder,它是一個編碼格式的標識,使用LATIN1還是UTF16,這個是在String生成的時候自動確定的,如果字符串中都是能用LATIN1編碼表示,那coder的值就是0,否則就是UTF16編碼,coder的值就是1。

  可以看到JDK9在這方面的優(yōu)化,在較多情況下不包含那些奇奇怪怪的字符的時候,足以應付,而這個空間卻小了1byte,實現(xiàn)了String空間的壓縮。

  2 String常量池的演變

  2.1 StringTable變化

String 的 String Pool是一個固定大小的 Hashtable。
   
    在jdk6中,StringTable的長度固定為1009。
    如果放進 String Pool的String非常多,就會造成Hash沖突嚴重,從而導致鏈表會很長,而鏈表長了后直接會造成的影響就是當調用 intern() 時性能會大幅下降。
   
    從jdk7起,StringTable的長度默認值是60013。
   
    使用-XX:StringTableSize可設置StringTable的長度。    
    在jdk8之前,對StringTableSize的設置沒有最小限制。
    jdk8開始,StringTable可設置的最小值是1009。
       

驗證:
    通過 jps 命令查看進程號
    使用 jinfo -flag StringTableSize 進程號 查看StringTable大小    
```

  2.2 內(nèi)存位置變化

Java6及以前,字符串常量池存放在永久代。
   
Java7開始,字符串常量池的位置調整到Java堆內(nèi)。
所有的字符串都保存在堆(Heap)中,和其他普通對象一樣,這樣在進行調優(yōu)應用時僅需要調整堆大小就可以了。
```

  官網(wǎng)說明

  https://www.oracle.com/technetwork/java/javase/jdk7-relnotes-418459.html#jdk7changes

1661409127968_3.jpg

  JDK6環(huán)境下測試:

/*
    jdk6中,修改JVM內(nèi)存大?。?
    -XX:PermSize=6m -XX:MaxPermSize=6m -Xms6m -Xmx6m
 */
public class StringTableTest {
    public static void main(String[] args) {
        Set<String> set = new HashSet<String>();

        int i=0;
        while (true){
            set.add(String.valueOf(i++).intern());
        }
    }
}

執(zhí)行結果異常信息:
    Exception in thread "main" java.lang.OutOfMemoryError: PermGen space
    at java.lang.String.intern(Native Method)
```

  JDK7環(huán)境下測試:

/*
    jdk7中,修改JVM內(nèi)存大?。?
    -XX:PermSize=6m -XX:MaxPermSize=6m -Xms6m -Xmx6m -XX:-UseGCOverheadLimit
 */
public class StringTableTest {
    public static void main(String[] args) {
        Set<String> set = new HashSet<String>();

        int i=0;
        while (true){
            set.add(String.valueOf(i++).intern());
        }
    }
}

執(zhí)行結果異常信息:
    Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
    at java.lang.Integer.toString(Integer.java:331)
    at java.lang.String.valueOf(String.java:2954)
    at StringTableTest.main(StringTableTest.java:14)
```

  3 String的拼接原理

  3.1 拼接原理

  源代碼:

public static void main(String[] args) {
    String s1 ="hello";
    String s2 ="world";
    String s3 = s1+s2;
    System.out.println(s3);
}

```

  使用 JDK8 編譯后字節(jié)碼:

0 ldc #2 <hello>
 2 astore_1
 3 ldc #3 <world>
 5 astore_2
 6 new #4 <java/lang/StringBuilder>
 9 dup
10 invokespecial #5 <java/lang/StringBuilder.<init>>
13 aload_1
14 invokevirtual #6 <java/lang/StringBuilder.append>
17 aload_2
18 invokevirtual #6 <java/lang/StringBuilder.append>
21 invokevirtual #7 <java/lang/StringBuilder.toString>
24 astore_3
25 getstatic #8 <java/lang/System.out>
28 aload_3
29 invokevirtual #9 <java/io/PrintStream.println>
32 return
```

  使用 JDK9 編譯后字節(jié)碼:

0 ldc #2 <hello>
 2 astore_1
 3 ldc #3 <world>
 5 astore_2
 6 aload_1
 7 aload_2
 8 invokedynamic #4 <makeConcatWithConstants, BootstrapMethods #0>
13 astore_3
14 getstatic #5 <java/lang/System.out>
17 aload_3
18 invokevirtual #6 <java/io/PrintStream.println>
21 return
```

  結論:

  ```java

  JDK8及之前,字符串變量的拼接,底層使用的是StringBuilder對象,利用append方法進行拼接。

  (注:jdk1.4之前使用StringBuffer)

  JDK9以后的編譯器已經(jīng)改成使用動態(tài)指令invokedynamic,

  調用StringConcatFactory.makeConcatWithConstants方法進行字符串拼接優(yōu)化。

  ```

  3.2 核心方法

makeConcatWithConstants方法在StringConcatFactory類中定義。
       
    makeConcatWithConstants內(nèi)部調用了doStringConcat,
    而doStringConcat方法則調用了generate方法來生成MethodHandle;
    generate根據(jù)不同的STRATEGY來生成MethodHandle,這些STRATEGY(策略)有
        BC_SB(等價于JDK8的優(yōu)化方式)
        BC_SB_SIZED
        BC_SB_SIZED_EXACT
        MH_SB_SIZED
        MH_SB_SIZED_EXACT
        MH_INLINE_SIZED_EXACT(默認)

    前五種策略本質還是用StringBuilder的實現(xiàn),而默認的策略MH_INLINE_SIZED_EXACT是直接使用字節(jié)數(shù)組來操作,并且字節(jié)數(shù)組長度預先計算好,可以減少字符串復制操作。
   
    可以通過添加JVM參數(shù)來改變默認的策略,例如將策略改為BC_SB
        -Djava.lang.invoke.stringConcat=BC_SB
        -Djava.lang.invoke.stringConcat.debug=true

```

  源碼:

  ==makeConcatWithConstants內(nèi)部調用了doStringConcat方法==

1661409368192_4.jpg

  ==doStringConcat方法則調用了generate方法來生成MethodHandle==

1661409379481_5.jpg

  ==generate根據(jù)不同的STRATEGY來生成MethodHandle==

1661409392833_6.jpg

  ==這些STRATEGY(策略)分別是==

private enum Strategy {
        /**
         * Bytecode generator, calling into {@link java.lang.StringBuilder}.
         */
        BC_SB,

        /**
         * Bytecode generator, calling into {@link java.lang.StringBuilder};
         * but trying to estimate the required storage.
         */
        BC_SB_SIZED,

        /**
         * Bytecode generator, calling into {@link java.lang.StringBuilder};
         * but computing the required storage exactly.
         */
        BC_SB_SIZED_EXACT,

        /**
         * MethodHandle-based generator, that in the end calls into {@link java.lang.StringBuilder}.
         * This strategy also tries to estimate the required storage.
         */
        MH_SB_SIZED,

        /**
         * MethodHandle-based generator, that in the end calls into {@link java.lang.StringBuilder}.
         * This strategy also estimate the required storage exactly.
         */
        MH_SB_SIZED_EXACT,

        /**
         * MethodHandle-based generator, that constructs its own byte[] array from
         * the arguments. It computes the required storage exactly.
         */
        MH_INLINE_SIZED_EXACT
    }
```

  ==默認的策略MH_INLINE_SIZED_EXACT==

1661409443879_7.jpg

  3.3 常見筆試題

/*
    產(chǎn)生2個字符串對象:字符串常量池中一個,堆內(nèi)存中一個。
*/
String s = new String("abc");



/*
    產(chǎn)生1個字符串對象:常量池中的"abc"。
    代碼在編譯階段會優(yōu)化為 String s = "abc";
*/
String s = "a"+"b"+"c";



/*
    5個字符串對象
    常量池:"a", "b"
    堆內(nèi)存:new方式的"a",new方式的"b",new方式的"ab"
    注意:常量池中不會產(chǎn)生"ab"
*/
String s = new String("a") + new String("b");



/*
jdk8及之前創(chuàng)建3個字符串對象:
    常量池: "c" , "ab"
    堆中: new "abc"

jdk9之后創(chuàng)建2個字符串對象:
    常量池: "c"
    堆中: new "abc"
*/
String s1 = "c";
String s2 = "a"+"b"+s1;

```

  4 intern()方法的演變

  4.1 intern()方法調用區(qū)別

public class StringDemo5 {
    public static void main(String[] args) {
        String s1 = new String("ab");
        String s2 = "ab";
        System.out.println(s1==s2); //fasle


        //intern()方法從常量池中取出"ab"對象
        String s1 = new String("ab").intern();
        String s2 = "ab";
        System.out.println(s1==s2); //true

         /*
            從常量池中取出和s1內(nèi)容相同的"ab"對象,此時常量池中沒有"ab"對象。

            如果常量池中沒有該字符串對象:
                jdk6及之前,intern()方法會創(chuàng)建新的字符串對象,放入常量池并返回新的地址。
                jdk7及之后,intern()方法會將調用者對象的地址放入常量池,并返回調用者對象地址。
         */
        String s1 = new String("a") + new String("b");
        s1.intern();
        String s2 = "ab";
        System.out.println(s1==s2); //jdk6 false;  jdk7之后true
    }

}
```

  4.2 intern()方法總結

  ```java

  intern()方法將這個字符串對象嘗試放入常量池中,并返回地址。

  jdk1.6中:

  如果池中有,則不會放入,返回已有的池中的對象的地址。

  如果池中沒有,則把此對象重新創(chuàng)建一份,放入池中,并返回池中新的對象地址。

  jdk1.7起:

  如果池中有,則不會放入,返回已有的池中的對象的地址。

  如果池中沒有,則把此對象的引用地址復制一份,放入池中,并返回池中的引用地址。

  ```

分享到:
在線咨詢 我要報名
和我們在線交談!