第 13 章 StringTable

孫小聖98發表於2020-11-27

第 13 章 StringTable

1、String 的基本特性

1.1、String 概述

String 的概述

  1. String:字串,使用一對 “” 引起來表示

    String s1 = "mogublog" ;   			// 字面量的定義方式
    String s2 =  new String("moxi");     // new 物件的方式
    
  2. String宣告為final的,不可被繼承

  3. String實現了Serializable介面:表示字串是支援序列化的。實現了Comparable介面:表示String可以比較大小

  4. string在jdk8及以前內部定義了final char[] value用於儲存字串資料。JDK9時改為byte[]

為什麼 JDK9 改變了 String 的結構

官方文件

http://openjdk.java.net/jeps/254


為什麼改為 byte[] 儲存?

  1. String類的當前實現將字元儲存在char陣列中,每個字元使用兩個位元組(16位)。
  2. 從許多不同的應用程式收集的資料表明,字串是堆使用的主要組成部分,而且大多數字符串物件只包含拉丁字元。這些字元只需要一個位元組的儲存空間,因此這些字串物件的內部char陣列中有一半的空間將不會使用。
  3. 之前 String 類使用 UTF-16 的 char[] 陣列儲存,現在改為 byte[] 陣列 外加一個編碼標誌位儲存,該編碼標誌將指定 String 類中 byte[] 陣列的編碼方式
  4. 結論:String再也不用char[] 來儲存了,改成了byte [] 加上編碼標記,節約了一些空間
  5. 同時基於String的資料結構,例如StringBuffer和StringBuilder也同樣做了修改
// 之前
private final char value[];
// 之後
private final byte[] value

1.2、String 的基本特徵

String 的基本特徵

String:代表不可變的字元序列。簡稱:不可變性。

  1. 當對字串重新賦值時,需要重寫指定記憶體區域賦值,不能使用原有的value進行賦值。
  2. 當對現有的字串進行連線操作時,也需要重新指定記憶體區域賦值,不能使用原有的value進行賦值。
  3. 當呼叫String的replace()方法修改指定字元或字串時,也需要重新指定記憶體區域賦值,不能使用原有的value進行賦值。

通過字面量的方式(區別於new)給一個字串賦值,此時的字串值宣告在字串常量池中。


當對字串重新賦值時,需要重寫指定記憶體區域賦值,不能使用原有的value進行賦值

  • 程式碼
@Test
public void test1() {
    String s1 = "abc";//字面量定義的方式,"abc"儲存在字串常量池中
    String s2 = "abc";
    s1 = "hello";

    System.out.println(s1 == s2);//判斷地址:false

    System.out.println(s1);//hello
    System.out.println(s2);//abc
}
  • 位元組碼指令
    • 取字串 “abc” 時,使用的是同一個符號引用:#2
    • 取字串 “hello” 時,使用的是另一個符號引用:#3
 0 ldc #2 <abc>
 2 astore_1
 3 ldc #2 <abc>
 5 astore_2
 6 ldc #3 <hello>
 8 astore_1
 9 getstatic #4 <java/lang/System.out>
12 aload_1
13 aload_2
14 if_acmpne 21 (+7)
17 iconst_1
18 goto 22 (+4)
21 iconst_0
22 invokevirtual #5 <java/io/PrintStream.println>
25 getstatic #4 <java/lang/System.out>
28 aload_1
29 invokevirtual #6 <java/io/PrintStream.println>
32 getstatic #4 <java/lang/System.out>
35 aload_2
36 invokevirtual #6 <java/io/PrintStream.println>
39 return

當對現有的字串進行連線操作時,也需要重新指定記憶體區域賦值,不能使用原有的value進行賦值

  • 程式碼
@Test
public void test2() {
    String s1 = "abc";
    String s2 = "abc";
    s2 += "def";
    System.out.println(s2);//abcdef
    System.out.println(s1);//abc
}
  • 位元組碼指令:拼接操作通過 StringBuilder 的 append() 方法完成
 0 ldc #2 <abc>
 2 astore_1
 3 ldc #2 <abc>
 5 astore_2
 6 new #7 <java/lang/StringBuilder>
 9 dup
10 invokespecial #8 <java/lang/StringBuilder.<init>>
13 aload_2
14 invokevirtual #9 <java/lang/StringBuilder.append>
17 ldc #10 <def>
19 invokevirtual #9 <java/lang/StringBuilder.append>
22 invokevirtual #11 <java/lang/StringBuilder.toString>
25 astore_2
26 getstatic #4 <java/lang/System.out>
29 aload_2
30 invokevirtual #6 <java/io/PrintStream.println>
33 getstatic #4 <java/lang/System.out>
36 aload_1
37 invokevirtual #6 <java/io/PrintStream.println>
40 return

當呼叫string的replace()方法修改指定字元或字串時,也需要重新指定記憶體區域賦值,不能使用原有的value進行賦值

@Test
public void test3() {
    String s1 = "abc";
    String s2 = s1.replace('a', 'm');
    System.out.println(s1);//abc
    System.out.println(s2);//mbc
}

來看看 replace() 方法的原始碼

  • new String(buf, true); 後,返回新的 String 物件
public String replace(char oldChar, char newChar) {
    if (oldChar != newChar) {
        int len = value.length;
        int i = -1;
        char[] val = value; /* avoid getfield opcode */

        while (++i < len) {
            if (val[i] == oldChar) {
                break;
            }
        }
        if (i < len) {
            char buf[] = new char[len];
            for (int j = 0; j < i; j++) {
                buf[j] = val[j];
            }
            while (i < len) {
                char c = val[i];
                buf[i] = (c == oldChar) ? newChar : c;
                i++;
            }
            return new String(buf, true);
        }
    }
    return this;
}

課後練習:String 的不可變性

  • 程式碼
/**
 * @author shkstart  shkstart@126.com
 * @create 2020  23:44
 */
public class StringExer {
    String str = new String("good");
    char[] ch = {'t', 'e', 's', 't'};

    public void change(String str, char ch[]) {
        str = "test ok";
        ch[0] = 'b';
    }

    public static void main(String[] args) {
        StringExer ex = new StringExer();
        ex.change(ex.str, ex.ch);
        System.out.println(ex.str);//good
        System.out.println(ex.ch);//best
    }
}
  • str 的內容並沒有變:“test ok” 位於字串常量池中的另一個區域(地址),進行賦值操作並沒有修改原來 str 指向的引用的內容
good
best

1.3、String 的底層結構

String 底層 Hashtable 結構的說明

字串常量池是不會儲存相同內容的字串的

  1. String的String Pool是一個固定大小的Hashtable,預設值大小長度是1009。如果放進String Pool的String非常多,就會造成Hash衝突嚴重,從而導致連結串列會很長,而連結串列長了後直接會造成的影響就是當呼叫String.intern()方法時效能會大幅下降。
  2. 使用-XX:StringTablesize可設定StringTable的長度
  3. 在JDK6中StringTable是固定的,就是1009的長度,所以如果常量池中的字串過多就會導致效率下降很快,StringTablesize設定沒有要求
  4. 在JDK7中,StringTable的長度預設值是60013,StringTablesize設定沒有要求
  5. 在JDK8中,StringTable的長度預設值是60013,StringTable可以設定的最小值為1009

程式碼示例:設定 StringTable 的長度

  • 程式碼
/**
 * -XX:StringTableSize=1009
 *
 * @author shkstart  shkstart@126.com
 * @create 2020  23:53
 */
public class StringTest2 {
    public static void main(String[] args) {
        // 測試StringTableSize引數
        System.out.println("我來打個醬油");
        try {
            Thread.sleep(1000000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

通過 -XX:StringTableSize 設定 StringTable 長度

  • JVM 引數
-XX:StringTableSize=6666
  • jinfo 檢視變數值
jps
jinfo -flag StringTableSize 程式id

image-20200730132234031


測試不同 StringTable 長度下,程式的效能

  • 程式碼
/**
 * -XX:StringTableSize=1009
 *
 * @author shkstart  shkstart@126.com
 * @create 2020  23:53
 */
public class StringTest2 {
    public static void main(String[] args) {       
        BufferedReader br = null;
        try {
            br = new BufferedReader(new FileReader("words.txt"));
            long start = System.currentTimeMillis();
            String data;
            while ((data = br.readLine()) != null) {
                //如果字串常量池中沒有對應data的字串的話,則在常量池中生成
                data.intern();
            }

            long end = System.currentTimeMillis();

            System.out.println("花費的時間為:" + (end - start));//1009:143ms  100009:47ms
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            if (br != null) {
                try {
                    br.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }

            }
        }
    }
}
  • -XX:StringTableSize=1009 :程式耗時 143ms
  • -XX:StringTableSize=100009 :程式耗時 47ms

2、String 的記憶體分配

2.1、String 記憶體分配演進過程

String 型別

  1. 在Java語言中有8種基本資料型別和一種比較特殊的型別String。這些型別為了使它們在執行過程中速度更快、更節省記憶體,都提供了一種常量池的概念。
  2. 常量池就類似一個Java系統級別提供的快取。8種基本資料型別的常量池都是系統協調的,String型別的常量池比較特殊。它的主要使用方法有兩種。
    • 直接使用雙引號宣告出來的String物件會直接儲存在常量池中。比如:String info="atguigu.com";
    • 如果不是用雙引號宣告的String物件,可以使用String提供的intern()方法。

String 記憶體分配的演進過程

  1. Java 6及以前,字串常量池存放在永久代
  2. Java 7中 Oracle的工程師對字串池的邏輯做了很大的改變,即將字串常量池的位置調整到Java堆內
    • 所有的字串都儲存在堆(Heap)中,和其他普通物件一樣,這樣可以讓你在進行調優應用時僅需要調整堆大小就可以了。
    • 字串常量池概念原本使用得比較多,但是這個改動使得我們有足夠的理由讓我們重新考慮在Java 7中使用String.intern()。
  3. Java8元空間,字串常量在堆

image-20200711093546398

image-20200711093558709

2.2、為什麼要調整 String 位置

StringTable 為什麼要調整?

官方文件

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


  1. 為什麼要調整位置?
    • 永久代的預設比較小
    • 永久代垃圾回收頻率低
    • 堆中空間足夠大,字串可被及時回收
  2. 在JDK 7中,interned字串不再在Java堆的永久代中分配,而是在Java堆的主要部分(稱為年輕代和年老代)中分配,與應用程式建立的其他物件一起分配。
  3. 此更改將導致駐留在主Java堆中的資料更多,駐留在永久生成中的資料更少,因此可能需要調整堆大小。

程式碼示例

  • 程式碼
/**
 * jdk6中:
 * -XX:PermSize=6m -XX:MaxPermSize=6m -Xms6m -Xmx6m
 *
 * jdk8中:
 * -XX:MetaspaceSize=6m -XX:MaxMetaspaceSize=6m -Xms6m -Xmx6m
 * @author shkstart  shkstart@126.com
 * @create 2020  0:36
 */
public class StringTest3 {
    public static void main(String[] args) {
        //使用Set保持著常量池引用,避免full gc回收常量池行為
        Set<String> set = new HashSet<String>();
        //在short可以取值的範圍內足以讓6MB的PermSize或heap產生OOM了。
        short i = 0;
        while(true){
            set.add(String.valueOf(i++).intern());
        }
    }
}
  • 異常日誌說:我真沒騙你,字串真的在堆中(JDK8)
"C:\Program Files\Java\jdk1.8.0_144\bin\java" -XX:MetaspaceSize=6m -XX:MaxMetaspaceSize=6m -Xms6m -Xmx6m "-javaagent:C:\Program Files\JetBrains\IntelliJ IDEA 2017.3.1\lib\idea_rt.jar=1799:C:\Program Files\JetBrains\IntelliJ IDEA 2017.3.1\bin" -Dfile.encoding=UTF-8 -classpath "C:\Program Files\Java\jdk1.8.0_144\jre\lib\charsets.jar;C:\Program Files\Java\jdk1.8.0_144\jre\lib\deploy.jar;C:\Program Files\Java\jdk1.8.0_144\jre\lib\ext\access-bridge-64.jar;C:\Program Files\Java\jdk1.8.0_144\jre\lib\ext\cldrdata.jar;C:\Program Files\Java\jdk1.8.0_144\jre\lib\ext\dnsns.jar;C:\Program Files\Java\jdk1.8.0_144\jre\lib\ext\jaccess.jar;C:\Program Files\Java\jdk1.8.0_144\jre\lib\ext\jfxrt.jar;C:\Program Files\Java\jdk1.8.0_144\jre\lib\ext\localedata.jar;C:\Program Files\Java\jdk1.8.0_144\jre\lib\ext\nashorn.jar;C:\Program Files\Java\jdk1.8.0_144\jre\lib\ext\sunec.jar;C:\Program Files\Java\jdk1.8.0_144\jre\lib\ext\sunjce_provider.jar;C:\Program Files\Java\jdk1.8.0_144\jre\lib\ext\sunmscapi.jar;C:\Program Files\Java\jdk1.8.0_144\jre\lib\ext\sunpkcs11.jar;C:\Program Files\Java\jdk1.8.0_144\jre\lib\ext\zipfs.jar;C:\Program Files\Java\jdk1.8.0_144\jre\lib\javaws.jar;C:\Program Files\Java\jdk1.8.0_144\jre\lib\jce.jar;C:\Program Files\Java\jdk1.8.0_144\jre\lib\jfr.jar;C:\Program Files\Java\jdk1.8.0_144\jre\lib\jfxswt.jar;C:\Program Files\Java\jdk1.8.0_144\jre\lib\jsse.jar;C:\Program Files\Java\jdk1.8.0_144\jre\lib\management-agent.jar;C:\Program Files\Java\jdk1.8.0_144\jre\lib\plugin.jar;C:\Program Files\Java\jdk1.8.0_144\jre\lib\resources.jar;C:\Program Files\Java\jdk1.8.0_144\jre\lib\rt.jar;C:\Users\Heygo\Desktop\JVMDemo\out\production\chapter13;D:\JavaTools\apache-maven-3.3.9\repository\junit\junit\4.12\junit-4.12.jar;D:\JavaTools\apache-maven-3.3.9\repository\org\hamcrest\hamcrest-core\1.3\hamcrest-core-1.3.jar" com.atguigu.java.StringTest3
Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
	at java.util.HashMap.resize(HashMap.java:703)
	at java.util.HashMap.putVal(HashMap.java:662)
	at java.util.HashMap.put(HashMap.java:611)
	at java.util.HashSet.add(HashSet.java:219)
	at com.atguigu.java.StringTest3.main(StringTest3.java:22)

Process finished with exit code 1

3、String 的基本操作

核心思想

Java語言規範裡要求完全相同的字串字面量,應該包含同樣的Unicode字元序列(包含同一份碼點序列的常量),並且必須是指向同一個String類例項。

題目一

  • 程式碼
/**
 * @author shkstart  shkstart@126.com
 * @create 2020  0:49
 */
public class StringTest4 {
    public static void main(String[] args) {
        System.out.println();//2330
        System.out.println("1");//2331
        System.out.println("2");
        System.out.println("3");
        System.out.println("4");
        System.out.println("5");
        System.out.println("6");
        System.out.println("7");
        System.out.println("8");
        System.out.println("9");
        System.out.println("10");//2340

        //如下的字串"1" 到 "10"不會再次載入
        System.out.println("1");//2341
        System.out.println("2");//2341
        System.out.println("3");
        System.out.println("4");
        System.out.println("5");
        System.out.println("6");
        System.out.println("7");
        System.out.println("8");
        System.out.println("9");
        System.out.println("10");//2341
    }
}
  • 分析字串常量池的變化

    • 程式啟動時已經載入了 2330 個字串常量

    image-20200730140509031

    • 載入 換行符

    image-20200730140633063

    • 載入了字串常量 “1”~“9”

    image-20200730140720761

    • 載入字串常量 “10”

    image-20200730140849292

    • 之後的字串"1" 到 "10"不會再次載入

    image-20200730140916540

    image-20200730140923967

題目二

  • 程式碼
/**
 * @author shkstart  shkstart@126.com
 * @create 2020  0:51
 */
class Memory {
    public static void main(String[] args) {//line 1
        int i = 1;//line 2
        Object obj = new Object();//line 3
        Memory mem = new Memory();//line 4
        mem.foo(obj);//line 5
    }//line 9

    private void foo(Object param) {//line 6
        String str = param.toString();//line 7
        System.out.println(str);
    }//line 8
}
  • 分析執行時記憶體(foo() 方法是例項方法,其實圖中少了一個 this 區域性變數)

image-20200730141106676

4、字串拼接操作

4.1、符串拼接操作的結論

字串拼接操作的結論

  1. 常量與常量的拼接結果在常量池,原理是編譯期優化
  2. 常量池中不會存在相同內容的變數
  3. 拼接前後,只要其中有一個是變數,結果就在堆中。變數拼接的原理是StringBuilder
  4. 如果拼接的結果呼叫intern()方法,則主動將常量池中還沒有的字串物件放入池中,並返回此物件地址
    • 如果存在,則返回字串在常量池中的地址
    • 如果字串常量池中不存在該字串,則在常量池中建立一份,並返回此物件的地址

常量與常量的拼接結果在常量池,原理是編譯期優化

  • 程式碼
@Test
public void test1() {
    String s1 = "a" + "b" + "c";//編譯期優化:等同於"abc"
    String s2 = "abc"; //"abc"一定是放在字串常量池中,將此地址賦給s2
    /*
     * 最終.java編譯成.class,再執行.class
     * String s1 = "abc";
     * String s2 = "abc"
     */
    System.out.println(s1 == s2); //true
    System.out.println(s1.equals(s2)); //true
}
  • 從位元組碼指令看出:編譯器做了優化,將 “a” + “b” + “c” 優化成了 “abc”
 0 ldc #2 <abc>
 2 astore_1
 3 ldc #2 <abc>
 5 astore_2
 6 getstatic #3 <java/lang/System.out>
 9 aload_1
10 aload_2
11 if_acmpne 18 (+7)
14 iconst_1
15 goto 19 (+4)
18 iconst_0
19 invokevirtual #4 <java/io/PrintStream.println>
22 getstatic #3 <java/lang/System.out>
25 aload_1
26 aload_2
27 invokevirtual #5 <java/lang/String.equals>
30 invokevirtual #4 <java/io/PrintStream.println>
33 return
  • IDEA 反編譯 class 檔案後,來看這個問題

image-20200730144025877


拼接前後,只要其中有一個是變數,結果就在堆中

呼叫 intern() 方法,則主動將字串物件存入字串常量池中,並將其地址返回

  • 程式碼
@Test
public void test2(){
    String s1 = "javaEE";
    String s2 = "hadoop";

    String s3 = "javaEEhadoop";
    String s4 = "javaEE" + "hadoop";//編譯期優化

    //如果拼接符號的前後出現了變數,則相當於在堆空間中new String(),具體的內容為拼接的結果:javaEEhadoop
    String s5 = s1 + "hadoop";
    String s6 = "javaEE" + s2;
    String s7 = s1 + s2;

    System.out.println(s3 == s4);//true
    System.out.println(s3 == s5);//false
    System.out.println(s3 == s6);//false
    System.out.println(s3 == s7);//false
    System.out.println(s5 == s6);//false
    System.out.println(s5 == s7);//false
    System.out.println(s6 == s7);//false

    //intern():判斷字串常量池中是否存在javaEEhadoop值,如果存在,則返回常量池中javaEEhadoop的地址;
    //如果字串常量池中不存在javaEEhadoop,則在常量池中載入一份javaEEhadoop,並返回此物件的地址。
    String s8 = s6.intern();
    System.out.println(s3 == s8);//true
}
  • 從位元組碼角度來看:拼接前後有變數,都會使用到 StringBuilder 類
  0 ldc #6 <javaEE>
  2 astore_1
  3 ldc #7 <hadoop>
  5 astore_2
  6 ldc #8 <javaEEhadoop>
  8 astore_3
  9 ldc #8 <javaEEhadoop>
 11 astore 4
 13 new #9 <java/lang/StringBuilder>
 16 dup
 17 invokespecial #10 <java/lang/StringBuilder.<init>>
 20 aload_1
 21 invokevirtual #11 <java/lang/StringBuilder.append>
 24 ldc #7 <hadoop>
 26 invokevirtual #11 <java/lang/StringBuilder.append>
 29 invokevirtual #12 <java/lang/StringBuilder.toString>
 32 astore 5
 34 new #9 <java/lang/StringBuilder>
 37 dup
 38 invokespecial #10 <java/lang/StringBuilder.<init>>
 41 ldc #6 <javaEE>
 43 invokevirtual #11 <java/lang/StringBuilder.append>
 46 aload_2
 47 invokevirtual #11 <java/lang/StringBuilder.append>
 50 invokevirtual #12 <java/lang/StringBuilder.toString>
 53 astore 6
 55 new #9 <java/lang/StringBuilder>
 58 dup
 59 invokespecial #10 <java/lang/StringBuilder.<init>>
 62 aload_1
 63 invokevirtual #11 <java/lang/StringBuilder.append>
 66 aload_2
 67 invokevirtual #11 <java/lang/StringBuilder.append>
 70 invokevirtual #12 <java/lang/StringBuilder.toString>
 73 astore 7
 75 getstatic #3 <java/lang/System.out>
 78 aload_3
 79 aload 4
 81 if_acmpne 88 (+7)
 84 iconst_1
 85 goto 89 (+4)
 88 iconst_0
 89 invokevirtual #4 <java/io/PrintStream.println>
 92 getstatic #3 <java/lang/System.out>
 95 aload_3
 96 aload 5
 98 if_acmpne 105 (+7)
101 iconst_1
102 goto 106 (+4)
105 iconst_0
106 invokevirtual #4 <java/io/PrintStream.println>
109 getstatic #3 <java/lang/System.out>
112 aload_3
113 aload 6
115 if_acmpne 122 (+7)
118 iconst_1
119 goto 123 (+4)
122 iconst_0
123 invokevirtual #4 <java/io/PrintStream.println>
126 getstatic #3 <java/lang/System.out>
129 aload_3
130 aload 7
132 if_acmpne 139 (+7)
135 iconst_1
136 goto 140 (+4)
139 iconst_0
140 invokevirtual #4 <java/io/PrintStream.println>
143 getstatic #3 <java/lang/System.out>
146 aload 5
148 aload 6
150 if_acmpne 157 (+7)
153 iconst_1
154 goto 158 (+4)
157 iconst_0
158 invokevirtual #4 <java/io/PrintStream.println>
161 getstatic #3 <java/lang/System.out>
164 aload 5
166 aload 7
168 if_acmpne 175 (+7)
171 iconst_1
172 goto 176 (+4)
175 iconst_0
176 invokevirtual #4 <java/io/PrintStream.println>
179 getstatic #3 <java/lang/System.out>
182 aload 6
184 aload 7
186 if_acmpne 193 (+7)
189 iconst_1
190 goto 194 (+4)
193 iconst_0
194 invokevirtual #4 <java/io/PrintStream.println>
197 aload 6
199 invokevirtual #13 <java/lang/String.intern>
202 astore 8
204 getstatic #3 <java/lang/System.out>
207 aload_3
208 aload 8
210 if_acmpne 217 (+7)
213 iconst_1
214 goto 218 (+4)
217 iconst_0
218 invokevirtual #4 <java/io/PrintStream.println>
221 return

4.2、字串拼接的底層細節

字串拼接的底層細節

程式碼示例 1

  • 程式碼
@Test
public void test3(){
    String s1 = "a";
    String s2 = "b";
    String s3 = "ab";
    /*
    如下的s1 + s2 的執行細節:(變數s是我臨時定義的)
    ① StringBuilder s = new StringBuilder();
    ② s.append("a")
    ③ s.append("b")
    ④ s.toString()  --> 約等於 new String("ab")

    補充:在jdk5.0之後使用的是StringBuilder,在jdk5.0之前使用的是StringBuffer
     */
    String s4 = s1 + s2;//"ab"
    System.out.println(s3 == s4);//false
}
  • 位元組碼指令
 0 ldc #14 <a>
 2 astore_1
 3 ldc #15 <b>
 5 astore_2
 6 ldc #16 <ab>
 8 astore_3
 9 new #9 <java/lang/StringBuilder>
12 dup
13 invokespecial #10 <java/lang/StringBuilder.<init>>
16 aload_1
17 invokevirtual #11 <java/lang/StringBuilder.append>
20 aload_2
21 invokevirtual #11 <java/lang/StringBuilder.append>
24 invokevirtual #12 <java/lang/StringBuilder.toString>
27 astore 4
29 getstatic #3 <java/lang/System.out>
32 aload_3
33 aload 4
35 if_acmpne 42 (+7)
38 iconst_1
39 goto 43 (+4)
42 iconst_0
43 invokevirtual #4 <java/io/PrintStream.println>
46 return
  • 分析拼接的步驟

    • new StringBuilder()
     9 new #9 <java/lang/StringBuilder>
    12 dup
    13 invokespecial #10 <java/lang/StringBuilder.<init>>
    
    • 載入字串變數,進行 append 操作
    16 aload_1
    17 invokevirtual #11 <java/lang/StringBuilder.append>
    20 aload_2
    21 invokevirtual #11 <java/lang/StringBuilder.append>
    24 invokevirtual #12 <java/lang/StringBuilder.toString>
    
    • 呼叫 StringBuilder 類的 toString() 方法,轉換為字串,並儲存在區域性變數中
    24 invokevirtual #12 <java/lang/StringBuilder.toString>
    27 astore 4
    

程式碼示例 2

  • 程式碼
/*
1. 字串拼接操作不一定使用的是StringBuilder!
   如果拼接符號左右兩邊都是字串常量或常量引用,則仍然使用編譯期優化,即非StringBuilder的方式。
2. 針對於final修飾類、方法、基本資料型別、引用資料型別的量的結構時,能使用上final的時候建議使用上。
 */
@Test
public void test4(){
    final String s1 = "a";
    final String s2 = "b";
    String s3 = "ab";
    String s4 = s1 + s2;
    System.out.println(s3 == s4);//true
}
  • 從位元組碼角度來看:為變數 s3 賦值時,直接使用 #16 符號引用,即字串常量 “ab”
 0 ldc #14 <a>
 2 astore_1
 3 ldc #15 <b>
 5 astore_2
 6 ldc #16 <ab>
 8 astore_3
 9 ldc #16 <ab>
11 astore 4
13 getstatic #3 <java/lang/System.out>
16 aload_3
17 aload 4
19 if_acmpne 26 (+7)
22 iconst_1
23 goto 27 (+4)
26 iconst_0
27 invokevirtual #4 <java/io/PrintStream.println>
30 return
  • IDEA 反編譯結果

image-20200730145819502


課後練習

  • 程式碼
//練習:
@Test
public void test5(){
    String s1 = "javaEEhadoop";
    String s2 = "javaEE";
    String s3 = s2 + "hadoop";
    System.out.println(s1 == s3);//false

    final String s4 = "javaEE";//s4:常量
    String s5 = s4 + "hadoop";
    System.out.println(s1 == s5);//true

}
  • 位元組碼指令:ldc #8 <javaEEhadoop>(帶 final 的變數在編譯時就已經確定了該變數的值,當做常量來處理)
 0 ldc #8 <javaEEhadoop>
 2 astore_1
 3 ldc #6 <javaEE>
 5 astore_2
 6 new #9 <java/lang/StringBuilder>
 9 dup
10 invokespecial #10 <java/lang/StringBuilder.<init>>
13 aload_2
14 invokevirtual #11 <java/lang/StringBuilder.append>
17 ldc #7 <hadoop>
19 invokevirtual #11 <java/lang/StringBuilder.append>
22 invokevirtual #12 <java/lang/StringBuilder.toString>
25 astore_3
26 getstatic #3 <java/lang/System.out>
29 aload_1
30 aload_3
31 if_acmpne 38 (+7)
34 iconst_1
35 goto 39 (+4)
38 iconst_0
39 invokevirtual #4 <java/io/PrintStream.println>
42 ldc #6 <javaEE>
44 astore 4
46 ldc #8 <javaEEhadoop>
48 astore 5
50 getstatic #3 <java/lang/System.out>
53 aload_1
54 aload 5
56 if_acmpne 63 (+7)
59 iconst_1
60 goto 64 (+4)
63 iconst_0
64 invokevirtual #4 <java/io/PrintStream.println>
67 return

拼接操作與 append 操作的效率對比

  • 程式碼
/*
體會執行效率:通過StringBuilder的append()的方式新增字串的效率要遠高於使用String的字串拼接方式!

分析原因:
    ① StringBuilder的append()的方式:
        自始至終中只建立過一個StringBuilder的物件
        使用String的字串拼接方式:建立過多個StringBuilder和String的物件
    ② 使用String的字串拼接方式:
        記憶體中由於建立了較多的StringBuilder和String的物件,記憶體佔用更大;
        如果進行GC,需要花費額外的時間。

 改進的空間:
    在實際開發中,如果基本確定要前前後後新增的字串長度不高於某個限定值highLevel的情況下,建議使用構造器例項化:
    StringBuilder s = new StringBuilder(highLevel);//new char[highLevel]
 */
@Test
public void test6(){

    long start = System.currentTimeMillis();

    // method1(100000);//4014
    method2(100000);//7

    long end = System.currentTimeMillis();

    System.out.println("花費的時間為:" + (end - start));
}

public void method1(int highLevel){
    String src = "";
    for(int i = 0;i < highLevel;i++){
        src = src + "a";//每次迴圈都會建立一個StringBuilder、String
    }
}

public void method2(int highLevel){
    //只需要建立一個StringBuilder
    StringBuilder src = new StringBuilder();
    for (int i = 0; i < highLevel; i++) {
        src.append("a");
    }
}
  1. 體會執行效率:通過StringBuilder的append()的方式新增字串的效率要遠高於使用String的字串拼接方式!
  2. 分析原因:
    1. StringBuilder的append()的方式:
      • 自始至終中只建立過一個StringBuilder的物件
      • 使用String的字串拼接方式:建立過多個StringBuilder和String的物件
    2. 使用String的字串拼接方式:
      • 記憶體中由於建立了較多的StringBuilder和String的物件,記憶體佔用更大;
      • 如果進行GC,需要花費額外的時間。
  3. 改進的空間:
    • 在實際開發中,如果基本確定要前前後後新增的字串長度不高於某個限定值highLevel的情況下,建議使用構造器例項化:
    • StringBuilder s = new StringBuilder(highLevel); //new char[highLevel]

通過位元組碼分析

  • method1() 方法的位元組碼指令:
    • 每次 for 迴圈都會建立一個 StringBuilder 物件
    • 呼叫 StringBuilder 的 toString() 方法又會建立新的 String 物件
 0 ldc #23
 2 astore_2
 3 iconst_0
 4 istore_3
 5 iload_3
 6 iload_1
 7 if_icmpge 36 (+29)
10 new #9 <java/lang/StringBuilder>
13 dup
14 invokespecial #10 <java/lang/StringBuilder.<init>>
17 aload_2
18 invokevirtual #11 <java/lang/StringBuilder.append>
21 ldc #14 <a>
23 invokevirtual #11 <java/lang/StringBuilder.append>
26 invokevirtual #12 <java/lang/StringBuilder.toString>
29 astore_2
30 iinc 3 by 1
33 goto 5 (-28)
36 return
  • method2() 方法的位元組碼指令:
 0 new #9 <java/lang/StringBuilder>
 3 dup
 4 invokespecial #10 <java/lang/StringBuilder.<init>>
 7 astore_2
 8 iconst_0
 9 istore_3
10 iload_3
11 iload_1
12 if_icmpge 28 (+16)
15 aload_2
16 ldc #14 <a>
18 invokevirtual #11 <java/lang/StringBuilder.append>
21 pop
22 iinc 3 by 1
25 goto 10 (-15)
28 return

關於 StringBuilder 構造器

  • StringBuilder 構造器:可傳入一個 int 型別的變數,用於初始化內部的 char[] 陣列
public StringBuilder(int capacity) {
	super(capacity);
}
  • AbstractStringBuilder(StringBuilder 的父類)的構造器
AbstractStringBuilder(int capacity) {
    value = new char[capacity];
}

5、intern() 的使用

5.1、intern() 方法的說明

intern() 方法的說明

先來點逼格,看看官方文件

When the intern method is invoked, if the pool already contains a string equal to this String object as determined by the 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.

public native String intern();

關於 intern() 方法的說明

  1. intern是一個native方法,呼叫的是底層C的方法

  2. 字串池最初是空的,由String類私有地維護。在呼叫intern方法時,如果池中已經包含了由equals(object)方法確定的與該字串物件相等的字串,則返回池中的字串。否則,該字串物件將被新增到池中,並返回對該字串物件的引用。

  3. 如果不是用雙引號宣告的String物件,可以使用String提供的intern方法:intern方法會從字串常量池中查詢當前字串是否存在,若不存在就會將當前字串放入常量池中。比如:

    String myInfo = new string("I love atguigu").intern();
    
  4. 也就是說,如果在任意字串上呼叫String.intern方法,那麼其返回結果所指向的那個類例項,必須和直接以常量形式出現的字串例項完全相同。因此,下列表示式的值必定是true

    ("a"+"b"+"c").intern()=="abc"
    
  5. 通俗點講,Interned String就是確保字串在記憶體裡只有一份拷貝,這樣可以節約記憶體空間,加快字串操作任務的執行速度。注意,這個值會被存放在字串內部池(String Intern Pool)

5.2、new String() 的說明

new String(“ab”)會建立幾個物件?

  • 程式碼
/**
 * 題目:
 * new String("ab")會建立幾個物件?看位元組碼,就知道是兩個。
 *     一個物件是:new關鍵字在堆空間建立的
 *     另一個物件是:字串常量池中的物件"ab"。 位元組碼指令:ldc
 *
 * @author shkstart  shkstart@126.com
 * @create 2020  20:38
 */
public class StringNewTest {
    public static void main(String[] args) {
        String str = new String("ab");
    }
}
  • 位元組碼指令
 0 new #2 <java/lang/String>
 3 dup
 4 ldc #3 <ab>
 6 invokespecial #4 <java/lang/String.<init>>
 9 astore_1
10 return
  • 0 new #2 <java/lang/String>:在堆中建立了一個 String 物件
  • 4 ldc #3 <ab> :在字串常量池中放入 “ab”(如果之前字串常量池中沒有 “ab” 的話)

image-20200730213430075

new String(“a”) + new String(“b”) 會建立幾個物件?

  • 程式碼
/**
 * 思考:
 * new String("a") + new String("b")呢?
 *  物件1:new StringBuilder()
 *  物件2: new String("a")
 *  物件3: 常量池中的"a"
 *  物件4: new String("b")
 *  物件5: 常量池中的"b"
 *
 *  深入剖析: StringBuilder的toString():
 *      物件6 :new String("ab")
 *      強調一下,toString()的呼叫,在字串常量池中,沒有生成"ab"
 *
 * @author shkstart  shkstart@126.com
 * @create 2020  20:38
 */
public class StringNewTest {
    public static void main(String[] args) {
        String str = new String("a") + new String("b");
    }
}
  • 位元組碼指令
 0 new #2 <java/lang/StringBuilder>
 3 dup
 4 invokespecial #3 <java/lang/StringBuilder.<init>>
 7 new #4 <java/lang/String>
10 dup
11 ldc #5 <a>
13 invokespecial #6 <java/lang/String.<init>>
16 invokevirtual #7 <java/lang/StringBuilder.append>
19 new #4 <java/lang/String>
22 dup
23 ldc #8 <b>
25 invokespecial #6 <java/lang/String.<init>>
28 invokevirtual #7 <java/lang/StringBuilder.append>
31 invokevirtual #9 <java/lang/StringBuilder.toString>
34 astore_1
35 return
  • 位元組碼指令分析:
    1. 0 new #2 <java/lang/StringBuilder> :拼接字串會建立一個 StringBuilder 物件
    2. 7 new #4 <java/lang/String> :建立 String 物件,對應於 new String(“a”)
    3. 11 ldc #5 <a> :在字串常量池中放入 “a”(如果之前字串常量池中沒有 “a” 的話)
    4. 19 new #4 <java/lang/String> :建立 String 物件,對應於 new String(“b”)
    5. 23 ldc #8 <b> :在字串常量池中放入 “b”(如果之前字串常量池中沒有 “b” 的話)
    6. 31 invokevirtual #9 <java/lang/StringBuilder.toString> :呼叫 StringBuilder 的 toString() 方法,會生成一個 String 物件

image-20200730221929113


深入剖析 StringBuilder 的toString() 方法

  • toString() 方法
@Override
public String toString() {
    // Create a copy, don't share the array
    return new String(value, 0, count);
}
  • value 是個 char[] 陣列
char[] value;

5.3、有點難的面試題

有點難的面試題

  • 程式碼
/**
 * 如何保證變數s指向的是字串常量池中的資料呢?有兩種方式:
 * 方式一: String s = "shkstart";//字面量定義的方式
 * 方式二: 呼叫intern()
 *         String s = new String("shkstart").intern();
 *         String s = new StringBuilder("shkstart").toString().intern();
 *
 * @author shkstart  shkstart@126.com
 * @create 2020  18:49
 */
public class StringIntern {
    public static void main(String[] args) {
        String s = new String("1");
        s.intern();//這方法其實沒啥屌用,呼叫此方法之前,字串常量池中已經存在"1"
        String s2 = "1";

        /*
            jdk6:false   jdk7/8:false
            因為 s 指向堆空間中的 "1" ,s2 指向字元創常量池中的 "1"
         */
        System.out.println(s == s2);

        // 執行完下一行程式碼以後,字串常量池中,是否存在"11"呢?答案:不存在!!
        String s3 = new String("1") + new String("1");//s3變數記錄的地址為:new String("11")
        /*
            如何理解:jdk6:建立了一個新的物件"11",也就有新的地址。
                     jdk7:此時常量中並沒有建立"11",而是在常量池中記錄了指向堆空間中new String("11")的地址(節省空間)
         */
        s3.intern(); // 在字串常量池中生成"11"。
        String s4 = "11";//s4變數記錄的地址:使用的是上一行程式碼程式碼執行時,在常量池中生成的"11"的地址

        // jdk6:false  jdk7/8:true
        System.out.println(s3 == s4);
    }
}

記憶體分析

  • JDK6 :正常眼光判斷即可
    • new String() 即在堆中
    • str.intern() 則把字串放入常量池中

image-20200730214321992

  • JDK7/8 :這就有點不一樣了
    • new String() 即在堆中
    • str.intern() 則把字串放入常量池中,出於節省空間的目的,如果 str 不存在於字串常量池中,則將 str 在堆中的引用儲存在字串常量池中,沒錯,字串常量池中存的是 str 在堆中的引用,所以 s3 == s4 為 true

image-20200730214346907

面試題的擴充

/**
 * StringIntern.java中練習的擴充:
 *
 * @author shkstart  shkstart@126.com
 * @create 2020  22:10
 */
public class StringIntern1 {
    public static void main(String[] args) {
        //執行完下一行程式碼以後,字串常量池中,是否存在"11"呢?答案:不存在!!
        String s3 = new String("1") + new String("1");//new String("11")
        //在字串常量池中生成物件"11"
        String s4 = "11";
        String s5 = s3.intern();

        // s3 是堆中的 "ab" ,s4 是字串常量池中的 "ab"
        System.out.println(s3 == s4);//false

        // s5 是從字串常量池中取回來的引用,當然和 s4 相等
        System.out.println(s5 == s4);//true
    }
}

5.4、intern() 方法的總結

關於 intern() 的總結

  1. JDK1.6中,將這個字串物件嘗試放入串池。
    1. 如果串池中有,則並不會放入。返回已有的串池中的物件的地址
    2. 如果沒有,會把此物件複製一份,放入串池,並返回串池中的物件地址
  2. JDK1.7起,將這個字串物件嘗試放入串池。
    • 如果串池中有,則並不會放入。返回已有的串池中的物件的地址
    • 如果沒有,則會把物件的引用地址複製一份,放入串池,並返回串池中的引用地址

5.5、intern() 方法的練習

intern() 方法的課後練習

練習 1

  • 程式碼
/**
 * @author shkstart  shkstart@126.com
 * @create 2020  20:17
 */
public class StringExer1 {
    public static void main(String[] args) {
        //在下一行程式碼執行完以後,字串常量池中並沒有"ab"
        String s = new String("a") + new String("b");//new String("ab")
        /*
            jdk6中:在串池中建立一個字串"ab"
            jdk8中:串池中沒有建立字串"ab",而是建立一個引用,指向new String("ab"),將此引用返回
         */
        String s2 = s.intern();
        System.out.println(s2 == "ab");//jdk6:true  jdk8:true
        System.out.println(s == "ab");//jdk6:false  jdk8:true
    }
}
  • JDK 6 中:在串池中建立一個字串"ab"

image-20200730215914474

  • JDK 7/8 中:串池中沒有建立字串"ab",而是建立一個引用,指向new String(“ab”),將此引用返回

image-20200730215945396


練習 2

  • 程式碼
/**
 * @author shkstart  shkstart@126.com
 * @create 2020  20:17
 */
public class StringExer1 {
    public static void main(String[] args) {
        // 在這兒加一句
        String x = "ab";
        
        //在下一行程式碼執行完以後,字串常量池中並沒有"ab"
        String s = new String("a") + new String("b");//new String("ab")
        /*
            jdk6中:在串池中建立一個字串"ab"
            jdk8中:串池中沒有建立字串"ab",而是建立一個引用,指向new String("ab"),將此引用返回
         */
        String s2 = s.intern();
        System.out.println(s2 == "ab");//jdk6:true  jdk8:true
        System.out.println(s == "ab");//jdk6:false  jdk8:false
    }
}
  • 記憶體分析

image-20200730221434699


練習 3

  • 程式碼 1
/**
 *
 * @author shkstart  shkstart@126.com
 * @create 2020  20:26
 */
public class StringExer2 {
    public static void main(String[] args) {
        String s1 = new String("ab");//執行完以後,會在字串常量池中會生成"ab"
        s1.intern();
        String s2 = "ab";
        System.out.println(s1 == s2); // false
    }
}

  • 程式碼 2
/**
 *
 * @author shkstart  shkstart@126.com
 * @create 2020  20:26
 */
public class StringExer2 {
    // 物件記憶體地址可以使用System.identityHashCode(object)方法獲取
    public static void main(String[] args) {
        String s1 = new String("a") + new String("b");//執行完以後,不會在字串常量池中會生成"ab"
        System.out.println(System.identityHashCode(s1));
        s1.intern();
        System.out.println(System.identityHashCode(s1));
        String s2 = "ab";
        System.out.println(System.identityHashCode(s2));
        System.out.println(s1 == s2); // true
    }
}

/* 程式執行結果
    21685669
    21685669
    21685669
    true
*/

5.6、intern() 方法效率測試

intern() 的效率測試

  • 程式碼
/**
 * 使用intern()測試執行效率:空間使用上
 * 結論:對於程式中大量存在存在的字串,尤其其中存在很多重複字串時,使用intern()可以節省記憶體空間。
 *
 * @author shkstart  shkstart@126.com
 * @create 2020  21:17
 */
public class StringIntern2 {
    static final int MAX_COUNT = 1000 * 10000;
    static final String[] arr = new String[MAX_COUNT];

    public static void main(String[] args) {
        Integer[] data = new Integer[]{1,2,3,4,5,6,7,8,9,10};

        long start = System.currentTimeMillis();
        for (int i = 0; i < MAX_COUNT; i++) {
            // arr[i] = new String(String.valueOf(data[i % data.length]));
            arr[i] = new String(String.valueOf(data[i % data.length])).intern();

        }
        long end = System.currentTimeMillis();
        System.out.println("花費的時間為:" + (end - start));

        try {
            Thread.sleep(1000000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.gc();
    }
}
  • 直接 new String :由於每個 String 物件都是 new 出來的,所以程式需要維護大量存放在堆空間中的 String 例項,程式記憶體佔用也會變高

image-20200730223548597

  • 使用 intern() 方法:由於陣列中字串的引用都指向字串常量池中的字串,所以程式需要維護的 String 物件更少,記憶體佔用也更低

image-20200730223500027

結論

  1. 對於程式中大量使用存在的字串時,尤其存在很多已經重複的字串時,使用intern()方法能夠節省記憶體空間。
  2. 大的網站平臺,需要記憶體中儲存大量的字串。比如社交網站,很多人都儲存:北京市、海淀區等資訊。這時候如果字串都呼叫intern() 方法,就會很明顯降低記憶體的大小。

6、StringTable 的垃圾回收

  • 程式碼
/**
 * String的垃圾回收:
 * -Xms15m -Xmx15m -XX:+PrintStringTableStatistics -XX:+PrintGCDetails
 *
 * @author shkstart  shkstart@126.com
 * @create 2020  21:27
 */
public class StringGCTest {
    public static void main(String[] args) {
        for (int j = 0; j < 100000; j++) {
            String.valueOf(j).intern();
        }
    }
}
  • JVM 引數
-Xms15m -Xmx15m -XX:+PrintStringTableStatistics -XX:+PrintGCDetails
  • 程式日誌:
    • 在 PSYoungGen 區發生了垃圾回收
    • Number of entries 和 Number of literals 明顯沒有 100000
    • 以上兩點均說明 StringTable 區發生了垃圾回收
"C:\Program Files\Java\jdk1.8.0_144\bin\java" -Xms15m -Xmx15m -XX:+PrintStringTableStatistics -XX:+PrintGCDetails "-javaagent:C:\Program Files\JetBrains\IntelliJ IDEA 2017.3.1\lib\idea_rt.jar=11487:C:\Program Files\JetBrains\IntelliJ IDEA 2017.3.1\bin" -Dfile.encoding=UTF-8 -classpath "C:\Program Files\Java\jdk1.8.0_144\jre\lib\charsets.jar;C:\Program Files\Java\jdk1.8.0_144\jre\lib\deploy.jar;C:\Program Files\Java\jdk1.8.0_144\jre\lib\ext\access-bridge-64.jar;C:\Program Files\Java\jdk1.8.0_144\jre\lib\ext\cldrdata.jar;C:\Program Files\Java\jdk1.8.0_144\jre\lib\ext\dnsns.jar;C:\Program Files\Java\jdk1.8.0_144\jre\lib\ext\jaccess.jar;C:\Program Files\Java\jdk1.8.0_144\jre\lib\ext\jfxrt.jar;C:\Program Files\Java\jdk1.8.0_144\jre\lib\ext\localedata.jar;C:\Program Files\Java\jdk1.8.0_144\jre\lib\ext\nashorn.jar;C:\Program Files\Java\jdk1.8.0_144\jre\lib\ext\sunec.jar;C:\Program Files\Java\jdk1.8.0_144\jre\lib\ext\sunjce_provider.jar;C:\Program Files\Java\jdk1.8.0_144\jre\lib\ext\sunmscapi.jar;C:\Program Files\Java\jdk1.8.0_144\jre\lib\ext\sunpkcs11.jar;C:\Program Files\Java\jdk1.8.0_144\jre\lib\ext\zipfs.jar;C:\Program Files\Java\jdk1.8.0_144\jre\lib\javaws.jar;C:\Program Files\Java\jdk1.8.0_144\jre\lib\jce.jar;C:\Program Files\Java\jdk1.8.0_144\jre\lib\jfr.jar;C:\Program Files\Java\jdk1.8.0_144\jre\lib\jfxswt.jar;C:\Program Files\Java\jdk1.8.0_144\jre\lib\jsse.jar;C:\Program Files\Java\jdk1.8.0_144\jre\lib\management-agent.jar;C:\Program Files\Java\jdk1.8.0_144\jre\lib\plugin.jar;C:\Program Files\Java\jdk1.8.0_144\jre\lib\resources.jar;C:\Program Files\Java\jdk1.8.0_144\jre\lib\rt.jar;C:\Users\Heygo\Desktop\JVMDemo\out\production\chapter13;D:\JavaTools\apache-maven-3.3.9\repository\junit\junit\4.12\junit-4.12.jar;D:\JavaTools\apache-maven-3.3.9\repository\org\hamcrest\hamcrest-core\1.3\hamcrest-core-1.3.jar" com.atguigu.java3.StringGCTest
[GC (Allocation Failure) [PSYoungGen: 4096K->488K(4608K)] 4096K->716K(15872K), 0.0024275 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
Heap
 PSYoungGen      total 4608K, used 3883K [0x00000000ffb00000, 0x0000000100000000, 0x0000000100000000)
  eden space 4096K, 82% used [0x00000000ffb00000,0x00000000ffe50fb0,0x00000000fff00000)
  from space 512K, 95% used [0x00000000fff00000,0x00000000fff7a020,0x00000000fff80000)
  to   space 512K, 0% used [0x00000000fff80000,0x00000000fff80000,0x0000000100000000)
 ParOldGen       total 11264K, used 228K [0x00000000ff000000, 0x00000000ffb00000, 0x00000000ffb00000)
  object space 11264K, 2% used [0x00000000ff000000,0x00000000ff039010,0x00000000ffb00000)
 Metaspace       used 3472K, capacity 4496K, committed 4864K, reserved 1056768K
  class space    used 381K, capacity 388K, committed 512K, reserved 1048576K
SymbolTable statistics:
Number of buckets       :     20011 =    160088 bytes, avg   8.000
Number of entries       :     14158 =    339792 bytes, avg  24.000
Number of literals      :     14158 =    603200 bytes, avg  42.605
Total footprint         :           =   1103080 bytes
Average bucket size     :     0.708
Variance of bucket size :     0.711
Std. dev. of bucket size:     0.843
Maximum bucket size     :         6
StringTable statistics:
Number of buckets       :     60013 =    480104 bytes, avg   8.000
Number of entries       :     62943 =   1510632 bytes, avg  24.000
Number of literals      :     62943 =   3584040 bytes, avg  56.941
Total footprint         :           =   5574776 bytes
Average bucket size     :     1.049
Variance of bucket size :     0.824
Std. dev. of bucket size:     0.908
Maximum bucket size     :         5

Process finished with exit code 0

String.valueOf() 方法原始碼

  • String 類的 valueOf() 方法
public static String valueOf(int i) {
    return Integer.toString(i);
}
  • Integer.toString() 方法中執行了 new String() ,即在堆中建立了一個 String 物件
public static String toString(int i) {
    if (i == Integer.MIN_VALUE)
        return "-2147483648";
    int size = (i < 0) ? stringSize(-i) + 1 : stringSize(i);
    char[] buf = new char[size];
    getChars(i, size, buf);
    return new String(buf, true);
}

7、G1 中的 String 去重操作

官方文件

http://openjdk.java.net/jeps/192

String 去重操作的背景

  1. 背景:對許多Java應用(有大的也有小的)做的測試得出以下結果:
    • 堆存活資料集合裡面String物件佔了25%
    • 堆存活資料集合裡面重複的String物件有13.5%
    • String物件的平均長度是45
  2. 許多大規模的Java應用的瓶頸在於記憶體,測試表明,在這些型別的應用裡面,Java堆中存活的資料集合差不多25%是String物件。更進一步,這裡面差不多一半String物件是重複的,重複的意思是說:
  3. str1.equals(str2)= true。堆上存在重複的String物件必然是一種記憶體的浪費。這個專案將在G1垃圾收集器中實現自動持續對重複的String物件進行去重,這樣就能避免浪費記憶體。

String 去重的的具體實現

  1. 當垃圾收集器工作的時候,會訪問堆上存活的物件。對每一個訪問的物件都會檢查是否是候選的要去重的String物件。
  2. 如果是,把這個物件的一個引用插入到佇列中等待後續的處理。一個去重的執行緒在後臺執行,處理這個佇列。處理佇列的一個元素意味著從佇列刪除這個元素,然後嘗試去重它引用的String物件。
  3. 使用一個Hashtable來記錄所有的被String物件使用的不重複的char陣列。當去重的時候,會查這個Hashtable,來看堆上是否已經存在一個一模一樣的char陣列。
  4. 如果存在,String物件會被調整引用那個陣列,釋放對原來的陣列的引用,最終會被垃圾收集器回收掉。
  5. 如果查詢失敗,char陣列會被插入到Hashtable,這樣以後的時候就可以共享這個陣列了。

命令列選項

  1. UseStringDeduplication(bool) :開啟String去重,預設是不開啟的,需要手動開啟。
  2. PrintStringDeduplicationStatistics(bool) :列印詳細的去重統計資訊
  3. stringDeduplicationAgeThreshold(uintx) :達到這個年齡的String物件被認為是去重的候選物件

相關文章