String對象和String常量池


1. String的基本特性

  1. String:字符串,使用一對 「」 引發來表示html

    String s1 = "mogublog" ; // 字面量的定義方式 String s2 = new String("moxi"); // new 對象的方式java

  2. String聲明爲final的,不可被繼承 String實現了Serializable接口:表示字符串是支持序列化的。實現了Comparable接口:表示String能夠比較大小面試

  3. string在jdk8及之前內部定義了final char[] value用於存儲字符串數據。JDK9時改成byte[]數組

1.1 爲何String在jdk9 以後改變了其底層結構


  1. String類的jdk8以前的實現將字符存儲在char數組中,每一個字符使用兩個字節(16位)。緩存

  2. 可是從許多不一樣的應用程序收集的數據代表,字符串是堆使用的主要組成部分,並且大多數字符串對象只包含拉丁字符。這些字符只須要一個字節的存儲空間,所以這些字符串對象的內部char數組中有一半的空間將不會使用。 例如 存ab 這個字符,若是使用char 數組,就要分配兩個字符的空間,即 四個字節, 可是ab 做爲英文,自己只需佔用兩個字節便可數據結構

  3. 以前 String 類使用 UTF-16 的 char[] 數組存儲,如今改成 byte[] 數組 外加一個編碼標誌位存儲,該編碼標誌將指定 String 類中 byte[] 數組的編碼方式app

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

同時基於String的數據結構,例如StringBuffer和StringBuilder也一樣作了修改ide

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
}複製代碼

示意圖:1605876490266

如上圖所示,, 堆中的Object 對象在調用toString 方法後,將在String pool 中生成一個字符串對象,並返回給 頂層棧幀foo方法中的局部變量 str

String 內存分配的演進過程

  1. Java 6及之前,字符串常量池存放在永久代
  2. Java 7中 Oracle的工程師對字符串池的邏輯作了很大的改變,即將字符串常量池的位置調整到Java堆內
  • 全部的字符串都保存在堆(Heap)中,和其餘普通對象同樣,這樣可讓你在進行調優應用時僅須要調整堆大小就能夠了。
  • 字符串常量池概念本來使用得比較多,可是這個改動使得咱們有足夠的理由讓咱們從新考慮在Java 7中使用String.intern()

JDK6 : 1605875141440

JDK7:

1605875161992

爲何要調整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 個數:

1605876129330

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

1605876173836

以後就再沒增長過了:

1605876215302

測試不一樣的 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個對象

  • 對象1:new StringBuilder()
  • 對象2: new String("a")
  • 對象3: 常量池中的"a"
  • 對象4: new String("b")
  • 對象5: 常量池中的"b"
  • 對象6: toString方法建立的對象

深刻剖析: 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:

1605972768473

JDK7

1605972800862

擴展

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個對象

1605976733817

使用intern: 雖然每次也都建立了 String 對象, 可是最後返回的 倒是intern 方法返回的 字符串常量池中的,會被重用, 而由於數組中並無指向在堆中建立的String 對象,將在垃圾回收時 被銷燬,減小內存,查看內存數據也是如此

1605977024929

結論:

  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個對象

1606031843868

打印內容:

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

許多大規模的Java應用的瓶頸在於內存,測試代表,在這些類型的應用裏面,Java堆中存活的數據集合差很少25%是String對象。更進一步,這裏面差很少一半String對象是重複的,重複的意思是說:

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對象被認爲是去重的候選對象
相關文章
相關標籤/搜索