天天看點

JVM---StringTable(字元串常量池)StringTable(字元串常量池)

StringTable(字元串常量池)

字元串常量池

  • StringTable(字元串常量池)
    • String的基本特性
    • String 的底層結構
    • String 的記憶體配置設定
    • String 的基本操作
      • 字元串拼接操作
    • intern() 的使用
    • new String() 的說明
    • String 對象在不同版本中的記憶體分析?
    • intern() 方法的練習
    • intern () 的效率測試(空間角度)
    • StringTable 的垃圾回收

String的基本特性

  • String:字元串,使用一對 " " 引起來表示。
String s1 = "atguigu" ;   			// 字面量的定義方式
String s2 =  new String("hello");     // new 對象的方式
           
  • String 被聲明為 final 的,不可被繼承。
  • String 實作了 Serializable 接口:表示字元串是支援序列化的;實作了 Comparable 接口:表示 String 可以比較大小。
  • String 在 jdk8 及以前内部定義了 final char value[] 用于存儲字元串資料。JDK9 時改為 byte[] 。

為什麼 JDK9 改變了 String 的結構?

官方文檔:http://openjdk.java.net/jeps/254

為什麼改為 byte [] 存儲?

  1. String 類的目前實作将字元存儲在 char 數組中,每個字元使用兩個位元組(16位)。從許多不同的應用程式收集的資料表明,字元串是堆使用的主要組成部分,而且大多數字元串對象隻包含拉丁字元(Latin-1),這些字元隻需要一個位元組的存儲空間,是以這些字元串對象的内部 char 數組中有一半的空間将不會使用,産生了大量浪費;
  2. 之前 String 類使用 UTF-16 的 char[] 數組存儲,現在改為 byte[] 數組外加一個編碼辨別存儲。該編碼表示如果你的字元是 ISO-8859-1 或者 Latin-1 ,那麼隻需要一個位元組存儲。如果你是其它字元集,比如 UTF-8 ,仍然用兩個位元組存儲,這樣 String 再也不用 char[] 來存儲了,改成了 byte [] 加上編碼标記,節約了一些空間,同時基于 String 的資料結構,例如StringBuffe r和 StringBuilder 也同樣做了修改。
// 之前
private final char value[];
// 之後
private final byte[] value
           

String 的基本特性

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

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

    - 當對現有的字元串進行連接配接操作時,也需要重新指定記憶體區域指派,不能使用原有的 value 進行指派。

    - 當調用 String 的 replace() 方法修改指定字元或字元串時,也需要重新指定記憶體區域指派,不能使用原有的 value 進行指派。

  • 通過字面量的方式(差別于 new )給一個字元串指派,此時的字元串值聲明在字元串常量池中。
當對字元串重新指派時,需要重寫指定記憶體區域指派,不能使用原有的 value 進行指派。

示例代碼:重新指派

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

       System.out.println(s1 == s2);//判斷位址:true  --> false

       System.out.println(s1);//
       System.out.println(s2);//abc

   }
           

輸出:

false
hello
abc
           

位元組碼指令:

JVM---StringTable(字元串常量池)StringTable(字元串常量池)
  • 取字元串 “abc” 時,使用的是同一個符号引用:#2
  • 取字元串 “hello” 時,使用的是另一個符号引用:#3
當對現有的字元串進行連接配接操作時,也需要重新指定記憶體區域指派,不能使用原有的 value 進行指派。

示例代碼:字元串連接配接

@Test
   public void test2() {
       String s1 = "abc";
       String s2 = "abc";
       s2 += "def";
       System.out.println(s2);//abcdef
       System.out.println(s1);//abc
   }

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

示例代碼:調用 replace() 方法

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

一道筆試題

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 指向的引用的内容。

String 的底層結構

字元串常量池是不會存儲相同内容的字元串的。
  • String 的 String Pool(字元串常量池)是一個固定大小的 Hashtable ,預設值大小長度是1009。如果放進 String Pool 的 String 非常多,就會造成 Hash 沖突嚴重,進而導緻連結清單會很長,而連結清單長了後直接會造成的影響就是當調用 String.intern() 方法時性能會大幅下降。
  • 使用 -XX:StringTablesize 可設定 StringTable 的長度;
  • 在 JDK6 中 StringTable 是固定的,就是1009 的長度,是以如果常量池中的字元串過多就會導緻效率下降很快,而 StringTablesize 設定沒有要求;
  • 在 JDK7 中,StringTable 的長度預設值是 60013 ,StringTablesize 設定沒有要求;
  • 在 JDK8 中,StringTable 的長度預設值是 60013,StringTable 可以設定的最小值為1009。
    JVM---StringTable(字元串常量池)StringTable(字元串常量池)
    JDK8 下:
Error: Could not create the Java Virtual Machine.
Error: A fatal exception has occurred. Program will exit.
StringTable size of 10 is invalid; must be between 1009 and 1305843009213693951
           

測試不同 StringTable 長度下程式的性能,示例代碼如下:

/**
 * 産生10萬個長度不超過10的字元串,包含a-z,A-Z
 */
public class GenerateString {
    public static void main(String[] args) throws IOException {
        FileWriter fw =  new FileWriter("words.txt");

        for (int i = 0; i < 100000; i++) {
            //1 - 10
           int length = (int)(Math.random() * (10 - 1 + 1) + 1);
            fw.write(getString(length) + "\n");
        }

        fw.close();
    }

    public static String getString(int length){
        String str = "";
        for (int i = 0; i < length; i++) {
            //65 - 90, 97-122
            int num = (int)(Math.random() * (90 - 65 + 1) + 65) + (int)(Math.random() * 2) * 32;
            str += (char)num;
        }
        return str;
    }
}

           
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.intern(); //如果字元串常量池中沒有對應data的字元串的話,則在常量池中生成
            }

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

            }
        }
    }
}
           
  1. -XX:StringTableSize=1009 :程式耗時 505ms.
  2. -XX:StringTableSize=100009 :程式耗時 116ms.

String 的記憶體配置設定

  1. 在 Java 語言中有8種基本資料類型和一種比較特殊的類型 String 。這些類型為了使它們在運作過程中速度更快、更節省記憶體,都提供了一種常量池的概念。
  2. 常量池就類似一個 Java 系統級别提供的緩存。8種基本資料類型的常量池都是系統協調的,String 類型的常量池比較特殊。它的主要使用方法有兩種:
    • 直接使用雙引号聲明出來的 String 對象會直接存儲在常量池中。比如:String info=“atqq.com” ;
    • 如果不是用雙引号聲明的 String 對象,可以使用 String 提供的 intern() 方法。
  • Java 6 及以前,字元串常量池存放在永久代;
  • Java 7 中 将字元串常量池的位置調整到 Java 堆内;
  • 所有的字元串都儲存在堆(Heap)中,和其他普通對象一樣,這樣可以在進行調優應用時僅需要調整堆大小就可以了;
  • 字元串常量池概念原本使用得比較多,但是這個改動使得需要重新考慮在 Java 7 中使用String.intern();
  • Java 8 元空間,字元串常量在堆。

StringTable 為什麼要調整?

官方文檔:https://www.oracle.com/java/technologies/javase/jdk7-relnotes.html#jdk7changes
  • 永久代的預設空間大小比較小;
  • 永久代垃圾回收頻率低,大量的字元串無法及時回收,容易進行 Full GC 産生 STW 或者容易産生 OOM:PermGen Space;
  • 堆中空間足夠大,字元串可被及時回收。

在 JDK 7 中,interned 字元串不再在 Java 堆的永久代中配置設定,而是在 Java 堆的主要部分(稱為年輕代和年老代)中配置設定,與應用程式建立的其他對象一起配置設定。此更改将導緻駐留在主 Java 堆中的資料更多,駐留在永久生成中的資料更少,是以可能需要調整堆大小。

示例代碼:

/**
 * jdk6中:
 * -XX:PermSize=6m -XX:MaxPermSize=6m -Xms6m -Xmx6m
 *
 * jdk8中:
 * -XX:MetaspaceSize=6m -XX:MaxMetaspaceSize=6m -Xms6m -Xmx6m
 */
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)

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
           

String 的基本操作

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

示例1:

public class StringTest4 {
    public static void main(String[] args) {
        System.out.println();//2293
        System.out.println("1");//2294
        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");//2303
        //如下的字元串"1" 到 "10"不會再次加載
        System.out.println("1");//2304
        System.out.println("2");//2304
        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");//2304
    }
}

           

結論:加依次載完字元串"1"到"10" ,後面的字元串"1" 到 "10"不會再次加載。

字元串拼接操作

  • 常量與常量的拼接結果在常量池,原理是編譯期優化;
  • 常量池中不會存在相同内容的變量;
  • 拼接前後,隻要其中有一個是變量,結果就在堆中。變量拼接的原理是 StringBuilder ;
  • 如果拼接的結果調用 intern() 方法,根據該字元串是否在常量池中存在,分為:

    - 如果存在,則傳回字元串在常量池中的位址;

    - 如果字元串常量池中不存在該字元串,則在常量池中建立一份,并傳回此對象的位址。

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

示例代碼:

@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
           
  1. 拼接前後,隻要其中有一個是變量,結果就在堆中。而調用 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
           

字元串拼接的底層細節示例說明:

示例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;//
    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
           

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

從位元組碼角度來看:為變量 s4 指派時,直接使用 #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
           

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

@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
        }
//        System.out.println(src);

    }

    public void method2(int highLevel){
        //隻需要建立一個StringBuilder
        StringBuilder src = new StringBuilder();
        for (int i = 0; i < highLevel; i++) {
            src.append("a");
        }
//        System.out.println(src);
    }

           
  1. 體會執行效率:通過 StringBuilder 的 append() 的方式添加字元串的效率要遠高于使用 String 的字元串拼接方式!
  2. 原因:
    • StringBuilder 的 append() 的方式:自始至終中隻建立過一個 StringBuilder 的對象;
    • 使用 String 的字元串拼接方式:建立過多個 StringBuilder 和 String(調的 toString 方法)的對象,記憶體占用更大;如果進行 GC ,需要花費額外的時間(在拼接的過程中産生的一些中間字元串可能永遠也用不到,會産生大量垃圾字元串)。
  3. 改進的空間:
    • 在實際開發中,如果基本确定要前前後後添加的字元串長度不高于某個限定值 highLevel 的情況下,建議使用構造器執行個體化;
    • StringBuilder s = new StringBuilder(highLevel); //new char[highLevel]
    • 可以避免頻繁擴容。

intern() 的使用

intern() 方法的說明:

  • intern 是一個 native 方法,調用的是底層 C 的方法;
  • 字元串常量池最初是空的,由 String 類私有地維護。在調用 intern 方法時,如果池中已經包含了由 equals(object) 方法确定的與該字元串内容相等的字元串,則傳回池中的字元串位址。否則,該字元串對象将被添加到池中,并傳回對該字元串對象的位址(這是源碼裡的大概翻譯);
  • 如果不是用雙引号聲明的 String 對象,可以使用 String 提供的 intern 方法:intern 方法會從字元串常量池中查詢目前字元串是否存在,若不存在就會将目前字元串放入常量池中。比如:
  • 如果在任意字元串上調用 String.intern 方法,那麼其傳回結果所指向的那個類執行個體,必須和直接以常量形式出現的字元串執行個體完全相同。是以,下清單達式的值必定是 true ;
  • 通俗點講,interned String 就是確定字元串在記憶體裡隻有一份拷貝,這樣可以節約記憶體空間,加快字元串操作任務的執行速度。注意,這個值會被存放在字元串内部池(String Intern Pool)。

new String() 的說明

new String(“ab”)會建立幾個對象?

/**
 * 題目:
 * new String("ab")會建立幾個對象?看位元組碼,就知道是兩個。
 *     一個對象是:new關鍵字在堆空間建立的
 *     另一個對象是:字元串常量池中的對象"ab"。 位元組碼指令:ldc
 *
 */
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” 的話)

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"
 *
 */
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” 的話);
  4. 19 new #4 <java/lang/String> :建立 String 對象,對應于 new String(“b”);
  5. 23 ldc #8 :在字元串常量池中放入 “b”(如果之前字元串常量池中沒有 “b” 的話);
  6. 31 invokevirtual #9 <java/lang/StringBuilder.toString> :調用 StringBuilder 的 toString() 方法,會生成一個 String 對象。

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

**
 * 如何保證變量s指向的是字元串常量池中的資料呢?
 * 有兩種方式:
 * 方式一: String s = "shkstart";//字面量定義的方式
 * 方式二: 調用intern()
 *         String s = new String("shkstart").intern();
 *         String s = new StringBuilder("shkstart").toString().intern();
 *
 */
           

String 對象在不同版本中的記憶體分析?

public class StringIntern {
    public static void main(String[] args) {

        String s = new String("1");//這裡在字元串常量池中建立了1
        s.intern();
        String s2 = "1";
        //這裡其實是堆中的對象s與字元串常量池中的s2進行判斷
        System.out.println(s == s2);//jdk6:false   jdk7/8:false
        
        /*
         1、s3變量記錄的位址為:new String("11")
         2、經過上面的分析,已經知道在堆中有了一個new String("11")這樣的String對象,但是在字元串常量池中沒有"11"
         3、接着執行s3.intern(),在字元串常量池中生成"11"
           3-1、在JDK6的版本中,字元串常量池還在永久代,是以直接在永久代生成"11",也就有了新的位址
           3-2、而在JDK7的後續版本中,字元串常量池被移動到了堆中,此時堆裡已經有new String("11")了,出于節省空間的目的,直接将堆中的那個字元串的引用位址儲存在字元串常量池中。沒錯,字元串常量池中存的是new String("11")在堆中的位址
         4、是以在JDK7後續版本中,s3和s4指向的完全是同一個位址。
         */
        String s3 = new String("1") + new String("1");//pos_1
	    s3.intern();
        
        String s4 = "11";//s4變量記錄的位址:使用的是上一行代碼代碼執行時,在常量池中生成的"11"的位址
        System.out.println(s3 == s4);//jdk6:false  jdk7/8:true
    }

}
           

注:intern方法堆中對象的位址引用!!

JDK6 中輸出:

false
false
           

JDK6 中輸出:

false
true
           

為什麼輸出會不一樣呢?

JDK6 :

  • 一個是 new 建立的對象,一個是常量池中的對象,顯然不是同一個;
  • new String() 即在堆中;
  • str.intern() 則把字元串放入常量池中。
    JVM---StringTable(字元串常量池)StringTable(字元串常量池)

JDK7 之後:

JVM---StringTable(字元串常量池)StringTable(字元串常量池)
  • 對 s3 和 s4 來說,因為 s3 變量記錄的位址是 new String(“11”) ,然後這段代碼執行完以後,常量池中不存在 “11”,然後執行 s3.intern() 後,就會在常量池中生成 “11”,最後 s4 用的就是 s3 的位址。

拓展一下:

/**
 * StringIntern.java中練習的拓展:
 *
 */
public class StringIntern1 {
    public static void main(String[] args) {
        //執行完下一行代碼以後,字元串常量池中,是否存在"11"呢?答案:不存在!!
        String s3 = new String("1") + new String("1");//new String("11")
        //在字元串常量池中生成對象"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
    }
}
           

intern() 方法的練習

練習1:

public class StringExer1 {
    public static void main(String[] args) {
        String x = "ab";
        String s = new String("a") + new String("b");//new String("ab")
        //在上一行代碼執行完以後,字元串常量池中并沒有"ab"
		/*
		1、jdk6中:在字元串常量池(此時在永久代)中建立一個字元串"ab"
        2、jdk8中:字元串常量池(此時在堆中)中沒有建立字元串"ab",而是建立一個引用,指向new String("ab"),将此引用傳回
        3、詳解看上面
		*/
        String s2 = s.intern();

        System.out.println(s2 == "ab");//jdk6:true  jdk8:true
        System.out.println(s == "ab");//jdk6:false  jdk8:true
    }
}
           

JDK6:

JVM---StringTable(字元串常量池)StringTable(字元串常量池)

JDK7/8:

JVM---StringTable(字元串常量池)StringTable(字元串常量池)

練習2

public class StringExer1 {
    public static void main(String[] args) { 
        String x = "ab";
        String s = new String("a") + new String("b");//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
    }
}
           
JVM---StringTable(字元串常量池)StringTable(字元串常量池)

練習3

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

輸出結果:

1836019240
1836019240
1836019240
true
           

intern () 的效率測試(空間角度)

示例代碼:

/**
 - 使用intern()測試執行效率:空間使用上
 -  3. 結論:對于程式中大量存在存在的字元串,尤其其中存在很多重複字元串時,使用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();
        }
        System.gc();
    }
}
           
  • 直接 new String :由于每個 String 對象都是 new 出來的,是以程式需要維護大量存放在堆空間中的 String 執行個體,程式記憶體占用也會變高;
  • 使用 intern() 方法:由于數組中字元串的引用都指向字元串常量池中的字元串,是以程式需要維護的 String 對象更少,記憶體占用也更低;
//調用了intern()方法使用了字元串常量池裡的字元串,那麼前面堆裡的字元串便會被GC掉,這也是intern省記憶體的關鍵原因
arr[i] = new String(String.valueOf(data[i % data.length])).intern();
           

結論:

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

StringTable 的垃圾回收

示例代碼:

/**
 * String的垃圾回收:
 * -Xms15m -Xmx15m -XX:+PrintStringTableStatistics -XX:+PrintGCDetails
 */
public class StringGCTest {
    public static void main(String[] args) {
        for (int j = 0; j < 100000; j++) {
            String.valueOf(j).intern();
        }
    }
}
           

輸出結果:在 PSYoungGen 區發生了垃圾回收。

G1 中的 String 去重操作

官方文檔:http://openjdk.java.net/jeps/192

String去重操作的背景

注意不是字元串常量池的去重操作,字元串常量池本身就沒有重複的。
  1. 背景:對許多 Java 應用(有大的也有小的)做的測試得出以下結果:
    • 堆存活資料集合裡面 String 對象占了25%;
    • 堆存活資料集合裡面重複的 String 對象有13.5%;
    • String 對象的平均長度是45。
  2. 許多大規模的 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 對象被認為是去重的候選對象。