1. 程式人生 > 實用技巧 >String物件和String常量池

String物件和String常量池

1. String的基本特性

  1. String:字串,使用一對 “” 引起來表示
String s1 = "mogublog" ; // 字面量的定義方式 
String s2 = new String("moxi"); // new 物件的方式 
  1. String宣告為final的,不可被繼承 String實現了Serializable介面:表示字串是支援序列化的。實現了Comparable介面:表示String可以比較大小
  2. string在jdk8及以前內部定義了final char[] value用於儲存字串資料。JDK9時改為byte[]

1.1 為什麼String在jdk9 之後改變了其底層結構

官方解釋: http://openjdk.java.net/jeps/254

  1. String類的jdk8之前的實現將字元儲存在char陣列中,每個字元使用兩個位元組(16位)。

  2. 但是從許多不同的應用程式收集的資料表明,字串是堆使用的主要組成部分,而且大多數字符串物件只包含拉丁字元。這些字元只需要一個位元組的儲存空間,因此這些字串物件的內部char陣列中有一半的空間將不會使用。 例如 存ab 這個字元,如果使用char 陣列,就要分配兩個字元的空間,即 四個位元組, 但是ab 作為英文,本身只需佔用兩個位元組即可

  3. 之前 String 類使用 UTF-16 的 char[] 陣列儲存,現在改為 byte[] 陣列 外加一個編碼標誌位儲存,該編碼標誌將指定 String 類中 byte[] 陣列的編碼方式

結論:String再也不用char[] 來儲存了,改成了byte [] 加上編碼標記,節約了一些空間 ,

同時基於String的資料結構,例如StringBuffer和StringBuilder也同樣做了修改

1.2 String 的不可變性

  1. 當對字串變數重新賦值時,會直接新建一個字串(或池中本來就有的),不會影響本來的字串
  2. 當對現有的字串進行連線拼接操作時,也需要重新指定記憶體區域賦值,不能使用原有的value進行賦值。
  3. 當呼叫String的replace()方法修改指定字元或字串時,也需要重新指定記憶體區域賦值,不能使用原有的value進行賦值。

2. String的記憶體分配位置

在Java語言中有8種基本資料型別和一種比較特殊的非常常用的型別String。這些型別為了使它們在執行過程中速度更快、更節省記憶體,都提供了一種常量池的概念。

常量池就類似一個Java系統級別提供的快取。8種基本資料型別的常量池都是系統協調的,String型別的常量池比較特殊。它的主要使用方法有兩種。

  • 直接使用雙引號宣告出來的String物件會直接儲存在常量池中。比如:String info="atguigu.com";
  • 如果不是用雙引號宣告的String物件(new出來的,或者其他方法返回的),可以使用String提供的intern()方法。

程式碼演示:

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
}

示意圖:

如上圖所示,, 堆中的Object 物件在呼叫toString 方法後,將在String pool 中生成一個字串物件,並返回給 頂層棧幀foo方法中的區域性變數 str

String 記憶體分配的演進過程

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

JDK6 :

JDK7:

為什麼要調整String 常量池的位置呢

  • 永久代的預設比較小
  • 永久代垃圾回收頻率低
  • 堆中空間足夠大,字串可被及時回收

3 String 常量池的底層結構

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

  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

字串常量池中同一個字串只存在一份

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

程式碼示例:

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
    }
}

在第一波開始,堆中的String 個數:

第一波結束時, 字串個數,加了十個:

之後就再沒增加過了:

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

首先先建立一個擁有10W行不同字元的檔案,程式自行編寫,這裡就不演示了

/**
 * -XX:StringTableSize=1009
 */
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

4. 字串的拼接操作

  1. 常量與常量的拼接結果在常量池,在編譯期就進行運算了
  2. 常量池中不會存在相同內容的變數
  3. 拼接前後,只要其中有一個是變數,就會就在堆中new一個。不在常量池中(但是也會建立一個在常量池中),變數拼接的原理是StringBuilder
  4. 如果是new出來的String呼叫intern()方法,則會判斷常量池中有沒有這個字元
    • 如果存在,則返回字串在常量池中的地址
    • 如果字串常量池中不存在該字串,則在常量池中建立一份,並返回此物件的地址

4.1 兩個程式碼演示

程式碼一:

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
}

結果列印的都是 true,不管是 地址值還是 內容 都一樣,看下面解析出的位元組碼指令,在指令0的位置從常量池中直接載入"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

程式碼二:

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
    //後面都是false 的原因是,只要拼接時 有變數參與,都是相當於new 一個 不從常量池中共享
    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();
    //s6 雖然是堆中new出來的,但是呼叫intern方法返回出的是 常量池中的,所以和 s3 地址一樣
    System.out.println(s3 == s8);//true
}

4.2 字串變數拼接的底層實現

前面說到, 一旦拼接操作有變數引用的參與,而不是全部都是字面量的形式, 就會 在堆中 新new一個物件,這是為什麼呢?,下面用一個簡單的程式碼解釋

程式碼:

public void test3(){
    String s1 = "a";
    String s2 = "b";
    String s3 = "ab";
 
    String s4 = s1 + s2;//"ab"
    System.out.println(s3 == s4);//false
}

位元組碼指令:

0 ldc #14 <a>  //將字元a 從字串常量池中獲取
 2 astore_1  //放入 區域性變數索引為 1 的位置  (0為this)
 3 ldc #15 <b>  //將字元b從字串常量池中獲取
 5 astore_2  //放入 區域性變數索引為 2 的位置
 6 ldc #16 <ab>  //將字元ab 從字串常量池中獲取
 8 astore_3   //放入 區域性變數索引為 3 的位置
 9 new #9 <java/lang/StringBuilder>  // new 一個 StringBuilder物件,開闢空間
12 dup
13 invokespecial #10 <java/lang/StringBuilder.<init>>  //初始化該StringBuilder物件
16 aload_1  // 載入 區域性變量表中索引為1的值 ,也就是 a
17 invokevirtual #11 <java/lang/StringBuilder.append>  //呼叫 StringBuilder物件 的 append 方法
20 aload_2  //載入 b 
21 invokevirtual #11 <java/lang/StringBuilder.append> //同樣append
24 invokevirtual #12 <java/lang/StringBuilder.toString> //最後呼叫StringBuilder物件的toString方法
27 astore 4  //放入 區域性變量表 索引為 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

從上面的 位元組碼 逐行解釋中看, 帶有 變數的字串拼接,其底層是使用 StringBuilder 物件

相當於如下程式碼:

StringBuilder s = new StringBuilder();
s.append("a")
s.append("b")
s.toString()  //  約等於 new String("ab"),方法內就是 new String ,但是有些不同 後面說

補充:在jdk5.0之後使用的是StringBuilder,在jdk5.0之前使用的是StringBuffer

是不是所有字串變數的拼接操作都是使用的是StringBuilder

那肯定是不是的,看下面這種情況:

public void test4(){
    final String s1 = "a";
    final String s2 = "b";
    String s3 = "ab";
    String s4 = s1 + s2;
    System.out.println(s3 == s4);//true
}

位元組碼: 看 指令9的位置,為 s1+s2 的操作,並沒有使用 拼接的方式,而是在編譯器就已經處理好了

 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

結論: 和直接使用字面量相同, 如果拼接字元變數都為 final ,都是可以在編譯器就可以確定結果的, 所以也被編譯器優化了(在寫程式碼時,可以寫final的都可以寫上,優化程式碼)

4.3 拼接操作和使用StringBuilder拼接的效能差距

程式碼

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");
    }
}

我們發現, 使用method1方法,拼接字串10w次, 使用時間為 4014ms,

而使用method2方法,append 方式,僅僅只需7ms, 差距如此之大

為什麼?

  • StringBuilder的append()的方式:自始至終中只建立過一個StringBuilder的物件,並在操作結束後返回一個字串,操作過程中不會產生
  • 而 使用String的字串拼接方式:每一次拼接操作都會建立一個StringBuilder和String的物件,也就是20w個物件, 期間觸發GC的可能也很大, 進一步變慢

對於上面的StringBuilder方式,還有更一步的改進方式

檢視 StringBuilder 類底層的實現時,發現初始定義一個 char 型陣列(JDK8),用於append操作, 在陣列長度不夠時,會建立一個更長的,並進行拷貝, 這也有點浪費時間,所以在實際開發中,如果基本確定要前前後後新增的字串長度不高於某個限定值highLevel的情況下,建議使用構造器例項化:

StringBuilder s = new StringBuilder(highLevel); //new char[highLevel]

5. String的intern方法

5.1 intern()方法的基本說明

前面已經有使用過,並且大概解釋過,現在具體說明一下

public native String intern();
  1. intern是一個native方法,呼叫的是底層C的方法

  2. 字串池最初是空的。在呼叫intern方法時,如果池中已經包含了由equals(object)方法確定的與該字串物件相等的字串(也就是值相等),則返回池中的字串。否則,該字串物件值放一個到池中,並返回對該字串物件的引用。

  3. 也就是說,如果在任意字串上呼叫String.intern方法,那麼其返回結果所指向的那個類例項,必須和直接以字面量形式出現的字串例項完全相同。因此,下列表達式的值必定是true

    ("a"+"b"+"c").intern()=="abc"

  4. 通俗點講,String就是確保字串在常量池裡有一份拷貝,這樣可以節約記憶體空間,加快字串操作任務的執行速度。

如何保證 變數s 指向的是字串常量池中的資料呢?

方式一 : String s = "Hello"; //直接使用字面量

方式二: 使用 intern方法

  • String s = new String("Hello").intern()'
  • String s = new StringBuilder("123").toString().intern();

5.2 new String("ab") 建立幾個物件

這個面試題應該很多人都是知道的,答案是一個或者兩個, 下面用位元組碼指令證明這個事

public class StringNewTest {
    public static void main(String[] args) {
        String str = new String("ab");
    }
}

位元組碼指令:

 0 new #2 <java/lang/String>  //堆中建立 String物件
 3 dup
 4 ldc #3 <ab>  //從 常量池中獲取 ab 字串物件,如果沒有 則會建立
 6 invokespecial #4 <java/lang/String.<init>>  //使用 ab 字面量去初始化 堆中的 String 物件
 9 astore_1
10 return

所以有上面的結論 ,一個或兩個, 如果常量池中沒有這個字面量的情況是是會建立兩個的

看看 String類的帶參建構函式

private final char[] value;
private int hash;
//...
public String(String var1) {
  this.value = var1.value;
   this.hash = var1.hash;
}

可以看到,將常量池String物件中的value 屬性,也就是維護的char[] 和字元的hash值賦給了 new String 物件的 value屬性和 hash屬性,所以 new 出的String 物件的內容為引數中的值, 這也說明了,雖然 常量池中的String物件 和new 出的 String 物件本身地址值不同,但是他們所維護的char[]卻是同一個

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

public class StringNewTest {
    public static void main(String[] args) {
        String str = new String("a") + new String("b");
    }
}

位元組碼指令:

 0 new #2 <java/lang/StringBuilder> // 1. StringBuilder 物件
 3 dup
 4 invokespecial #3 <java/lang/StringBuilder.<init>>
 7 new #4 <java/lang/String> // 2. new String("a") 物件
10 dup
11 ldc #5 <a>  //3. 常量池中 a 字串物件
13 invokespecial #6 <java/lang/String.<init>> 
16 invokevirtual #7 <java/lang/StringBuilder.append>
19 new #4 <java/lang/String> // 4. new String 物件
22 dup
23 ldc #8 <b>  //5. 常量池中 b 字串物件
25 invokespecial #6 <java/lang/String.<init>>  
28 invokevirtual #7 <java/lang/StringBuilder.append>
31 invokevirtual #9 <java/lang/StringBuilder.toString>  //toString 返回的
34 astore_1
35 return
     

所有上面的程式碼 最多建立 6個物件

  • new String("a") + new String("b")呢?
  • 物件1:new StringBuilder()
  • 物件2: new String("a")
  • 物件3: 常量池中的"a"
  • 物件4: new String("b")
  • 物件5: 常量池中的"b"

深入剖析: StringBuilder的toString():

程式碼:

public String toString() {
    // Create a copy, don't share the array
    return new String(value, 0, count);
}

這是StringBuilder 物件的toString 方法, 將 StringBuilder 類中append 處理的char[] 連線為一個字串,

和普通的 new String("ab")不一樣, 並沒有使用字面量的形式建立,所以沒有在 常量池中建立"ab" 字串

所以這裡就建立了一個

5.3 JDK7 前後 intern() 方法的變化

在前面說到, intern() 方法是將判斷呼叫者字串物件的值 是否在常量池中存在,如果存在 則返回常量池中的那個物件引用,如果不存在則 造一個, 但是這個造一個 ,在隨著JDK7 將字串常量池移到堆空間時,發生了變化

程式碼示例:

public class StringIntern {
   public static void main(String[] args) {
       String s = new String("1");
       s.intern();//此方法呼叫前常量池中就已經有 字串 "1"了
       String s2 = "1";
       System.out.println(s == s2);  //jdk6:false   jdk7/8:false
       
     
       // 執行完下一行程式碼以後,字串常量池中,是否存在"11"呢?答案:不存在!!
       String s3 = new String("1") + new String("1");//s3變數記錄的地址為:new String("11")
       s3.intern(); 
       String s4 = "11";
       System.out.println(s3 == s4);// jdk6:false  jdk7/8:true
   }
}

上面的程式碼中

  • 第一個輸出語句 無論如何都是列印false,那是因為在建立 new String("1")時已經在堆中建立了 字串 "1",所以intern() 方法沒有再建立,而S2則引用了常量池中的物件,所以和 new String()s 一直都是false

  • 第二個輸出語句 在jdk6 中列印了 false, 是因為 程式碼 new String("1") + new String("1")相當於 建立了一個new String("11") 賦予了 s3 ,但是根據上一節說到的,並沒有在常量池中生成 "11",所以 當呼叫intern() 方法時, 建立了一個新的字串 "11" 放到了常量池中,注意 此時的環境為JDK6, 字串常量池在 方法區中,和new的物件不在一起,所以是建立一個新的方式

  • 但是在JDK7 及以後的版本,卻是列印 true,是因為 字串常量挪到了 堆中,和 new的物件在一起,所以杜絕空間浪費, 呼叫intern() 方法建立物件時,沒有拷貝新的,而是存了 new String() 物件的引用到字串常量池中,所以列印為 true

記憶體示意圖:

JDK6:

JDK7

擴充套件

public class StringIntern1 {
    public static void main(String[] args) {
        String s3 = new String("1") + new String("1");//new String("11")
        //在字串常量池中生成物件"11"
        String s4 = "11";
        String s5 = s3.intern();

        System.out.println(s3 == s4);//false

        System.out.println(s5 == s4);//true
    }
}

將上面的程式碼中 宣告字面量提到呼叫intern() 方法前後,列印結果就固定了,那是因為在宣告"11" 時已經建立一個新的字串到常量池中. 所以必定是false;

5.4 intern方法的總結

當呼叫 intern 方法時:

JDK1.6中,將這個字串物件嘗試放入串池。

  • 如果串池中有,則並不會放入。返回已有的串池中的物件的地址
  • 如果沒有,會把此物件複製一份,放入串池,並返回串池中的物件地址

JDK1.7起,將這個字串物件嘗試放入串池。

  • 如果串池中有,則並不會放入。返回已有的串池中的物件的地址
  • 如果沒有,則會把物件的引用地址複製一份,放入串池,並返回串池中的引用地址

5.5 intern() 方法效率測試

看下面的一段程式碼:

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();
        }
    }
}

上面的程式碼中,將 1-10 每次使用 String.valueOf 的方式轉換為 字串 存入陣列中,迴圈1000W次, 下面看 使用intern方法和不使用的區別

不使用: valueOf 方法中每次都在堆中new 了一個新的字串物件, 所以共建立了 1000w個物件

使用intern: 雖然每次也都建立了 String 物件, 但是最後返回的 卻是intern 方法返回的 字串常量池中的,會被重用, 而因為陣列中並沒有指向在堆中建立的String 物件,將在垃圾回收時 被銷燬,減少記憶體,檢視記憶體資料也是如此

結論:

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

6 String 的垃圾回收

程式碼演示: 建立10w個字串

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發生了垃圾回收的行為, 並且看出 字串常量池中也不足10w個物件

列印內容:

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

補充: G1中 String 去重操作

這個去重操作針對的不是 String本身,因為 String 常量池中 本身就不是重複的,而是針對 new String() 中維護的char[]

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物件被認為是去重的候選物件