結合JVM理解String

返回主博客

 

目錄

String基本特性

String的內存分配

String的基本操作

字符串拼接操作

intern使用

StringTable的垃圾回收

G1的String去重


String基本特性

  • 使用""表示,String str = ""  字面量形式,String str = new String("") 。
  • final 修飾,不可繼承
  • 實現了 java.io.Serializable。天然可以跨進程傳輸
  • 實現了 Comparable<String> 。可排序
  • 實現了 CharSequence。描述字符串結構的接口。
  • String代表了不可變的字符序列。
  • 字符串常量池不會存儲相同內容的字符串

char 改爲 byte

1.8之前使用char數組存儲,1.9之後使用的是byte數組。原因見http://openjdk.java.net/jeps/254 的Motivation和Description。這裏要了解一下decode。

Motivation

The current implementation of the String class stores characters in a char array, using two bytes (sixteen bits) for each character. Data gathered from many different applications indicates that strings are a major component of heap usage and, moreover, that most String objects contain only Latin-1 characters. Such characters require only one byte of storage, hence half of the space in the internal char arrays of such String objects is going unused.

Description

We propose to change the internal representation of the String class from a UTF-16 char array to a byte array plus an encoding-flag field. The new String class will store characters encoded either as ISO-8859-1/Latin-1 (one byte per character), or as UTF-16 (two bytes per character), based upon the contents of the string. The encoding flag will indicate which encoding is used.

之前String存儲字符是用char型數組,而char型數組是佔兩個字節(16位)的。從許多不同的應用程序收集的數據表明,字符串是堆使用的主要組成部分。而且,大多數字符串對象只包含拉丁字符。拉丁字符包括ASCII碼和IOS8859-1,IOS8859-1是歐洲碼範圍0-255,ASCII的範圍是0-127。所以這種字符只需要2^8=256,一個字節就能存下。這樣的話,使用這些字符編碼,會有一半空間浪費。將String類的內部表示形式從UTF-16字符數組更改爲byte數組加上編碼標誌字段。

同時,AbstractStringBuilderStringBuilder, 和 StringBuffer也做了相應修改:

String-related classes such as AbstractStringBuilderStringBuilder, and StringBuffer will be updated to use the same representation, as will the HotSpot VM's intrinsic string operations.

 

String不可變性案例

String s1 = "aaaa"
String s2 = "aaaa"
// s1 和 s2 都是從常量池獲取的同一個實例

s1 = s1 + "bbb";
// 將常量池中的"aaaa"和常量池中的"bbb"拼接創建一個新的實例,將其引用給到s1

s1.relace('a', 'b') 
// 也是創建一個新的對象 將其引用返回,"aaaa"並沒有改變
private void change(String str, char ch[]) {
    str = "test ok";
    char[0] = "b";
}

public void test () {
    str = "aaaa";
    char[] ch = {'t','e','s','t'};
    change(str, ch);
    System.out.println(str);// aaaa
    System.out.println(ch); // best
}

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

String的String Pool 是一個固定大小的Hashtable,默認大小爲1009。如果放進String Pool的的String非常多,機會導致hash衝突,從而導致鏈表變長,鏈表過長,會直接導致調用String.intern時性能下降。

使用-XX:StringTableSize 設置StringTable的長度。

jdk6中StringTable的大小固定的就是1009,如果String過多導致效率下降,StringTableSize設置多少都可以,沒有要求。

jdk7中,默認長度時60013,StringTableSize設置多少都可以,沒有要求。

jdk8後,1009是要求可以設置的最小值。

String的內存分配

在java語言中有8種基本數據類型和一種比較特殊的類型String。這些類型爲了使他們在運行過程中更快,更節省內存,都提供了一種常量池概念。

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

  • 直接使用雙引號申明出來的字面量而創建的String對象會直接存在常量池中
  • 可以通過intern()方法將運行時創建的String對象加入常量池

歷史:

  • java 6 及以前,字符串常量池存放在永久代。
  • java 7 中, 將字符串常量池的位置調整到java堆中。這樣調優的時候只需要調整堆就行了。使用intern方法也利於我們管理。
  • java 8 元空間,String對象存在堆中,將String常量的引用放在元空間。static的String引用和Class對象一起存在堆中。

爲什麼將StringTable調整到堆中

  • 整合Jrockit的需要
  • permSize默認比較小,大小也不太好調整,容易OOM
  • 將對象統一放置堆中,好管理,尤其是使用intern方法的時候。
  • 永久代垃圾回收頻率低

 

String的基本操作

System.out.println("111")
System.out.println("111")

這樣只會創建一個“111”的String對象在常量池。java規範中要求完全相同的字符串字面量,應該時具有相同的Unicode字符序列,並且必須指向同一個String實例。

字符串拼接操作

常量池不會存在相同內容常量

常量與常量的拼接,其結果在常量池,原理時編譯器優化。

String s1 = "a" + "b" + "c";
// 這種操作編譯期間就會優化成"abc"
String s2 = "abc";
// s1 == s2

拼接表達式中,只要有一個是變量,其結果就不在常量池,就是說該對象在堆中創建,但是不被StringTable所管理,可以理解爲常量池以外的堆空間,這種拼接,其實使用的是StringBuilder。

String s1 = "aaa";
String s2 = "bbb";
String s3 = "aaa" + "bbb";
String s4 = "aaa" + s2;
System.out.println(s4 == s3);// false "aaa" + s2;在非常量池堆空間,"aaa" + "bbb"在常量池堆空間。

 

拼接原理

1、存在 字面量拼接或者final(因爲修飾爲final,其賦值在編譯期間就已經完成)修飾引用的拼接,在編譯期間就已經完成拼接成字面量,這種拼接後的字符串存在常量池。如下案例。(開發中能使用final建議就加final)

    public void test3() {
        final String s1 = "aaa";
        final String s2 = "bbb";
        String s3 = s1 + s2;
        String s4 = "aaa" + "bbb";
        System.out.println(s3 ==  s4);
        // true
    }    
/**
     Code:
     stack=3, locals=5, args_size=1
     0: ldc           #2                  // String aaa
     2: astore_1
     3: ldc           #3                  // String bbb
     5: astore_2
     6: ldc           #8                  // String aaabbb
     8: astore_3
     9: ldc           #8                  // String aaabbb
     11: astore        4
     13: getstatic     #9                  // Field java/lang/System.out:Ljava/io/PrintStream;
     16: aload_3
     17: aload         4
     19: if_acmpne     26
     22: iconst_1
     23: goto          27
     26: iconst_0
     27: invokevirtual #10                 // Method java/io/PrintStream.println:(Z)V
     30: return
     */

2、存在變量引用(除final修飾外,final修飾就是指常量引用)拼接時,拼接時,其實是編譯爲class文件後,使用StringBuilder進行append。最後toString()返回。

    public void test1() {
        String s1 = "aaa";
        String s2 = "bbb";
        String s3 = s1 + s2;
    }
 /**
     Code:
     stack=2, locals=4, args_size=1
     0: ldc           #2                  // String aaa
     2: astore_1
     3: ldc           #3                  // String bbb
     5: astore_2
     6: new           #4                  // class java/lang/StringBuilder
     9: dup
     10: invokespecial #5                  // Method java/lang/StringBuilder."<init>":()V
     13: aload_1
     14: invokevirtual #6                  // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
     17: aload_2
     18: invokevirtual #6                  // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
     21: invokevirtual #7                  // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
     24: astore_3
     25: return
     */

補充

  • 這裏的拼接工具在jdk5.0之前用的是StringBuffer,爲了提升效率,在5.0之後用的是StringBuilder。
  • 如下圖。如果我們在循環體中字符串拼接,直接使用 ”+“ 號拼接,編譯器編譯之後的結果是,在每一次循環都會創建一個StringBuilder進行拼接,並且創建一個String對象返回。從而增加的對象的創建(我們知道new 是需要加鎖的),並且還會降低GC效率。因此,我們在循環體中拼接字符串,必須在循環體外顯示地new 一個StringBuilder,並在循環體中顯式的進行append。我循環10萬次,二者差距600倍(7:4014)
  • 如果我們拼接字符串時需要考慮線程安全問題,也是需要顯示地new StringBuffer進行append。
  • StringBuilder/StringBuffer的char[]的capacity 默認等於16,如果我們大致可以確定其大小,最好使用有參構造器。
    public void test2() {
        String s1 = "aaa";
        String s2 = "bbb";
        for (int i = 0; i < 100; i++) {
            s1 = s1 + s2;
        }
    }
/**
     Code:
     stack=2, locals=4, args_size=1
     0: ldc           #2                  // String aaa
     2: astore_1
     3: ldc           #3                  // String bbb
     5: astore_2
     6: iconst_0
     7: istore_3
     8: iload_3
     9: bipush        100
     11: if_icmpge     39
     14: new           #4                  // class java/lang/StringBuilder
     17: dup
     18: invokespecial #5                  // Method java/lang/StringBuilder."<init>":()V
     21: aload_1
     22: invokevirtual #6                  // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
     25: aload_2
     26: invokevirtual #6                  // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
     29: invokevirtual #7                  // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
     32: astore_1
     33: iinc          3, 1
     36: goto          8
     39: return

     */

 

intern使用

是一個native方法

  • 過程:判斷當前字符串的字符序列在StringTable中是否已經存在(使用equals方法),如果已經存在則,返回存在的已存在對象的引用,如果不存在,則在常量池中添加一個新的引用,該引用指向該對象(這裏注意不是創建一個新的對象,但是在jdk1.6會在永久代創建一個新的對象,然後返回這個引用),並將其引用返回。

案例

//在堆中創建對象"aaabbb"
        String s1 = new String(new String("aaa") + new String("bbb"));
        // StringTable中新增一個ref, 該ref == s8,  然後將該ref返回。最後 s2 == ref
        String s2 = s1.intern();
        // true
        System.out.println(s1 == s2);

        // 通過字面量形式在堆中創建一個"ccc"對象,將其引用給到StringTable
        String s3 = "ccc";
        // 在堆空間創建一個對象,該對象的char[] 和之前 的 "ccc" 一樣, 但是地址不同
        String s4 = new String(s3);
        // 發現StringTable中已經有了"ccc", 將其引用返回,但是,並沒有返回給s4,因此s4 != s3
        s4.intern();
        // false
        System.out.println(s3 == s4);
        
        String s6 = new String(new String("ddd") + new String("eee"));
        s6.intern();
        // 將StringTable 中 ref的引用給到s5
        String s5 = "dddeee";
        // true
        System.out.println(s5 == s6);

 

圖解

要理解原理,要先理解java的引用傳遞,StringTable中存儲的不過是字符串的地址。字符串的對象實際分散在堆空間的不同地方。

s1最初指向"aaabbb", s1.intern()後,stringtable[i] == s1(意思是,stringtable的第i位和s1一樣都指向”aaabbbb“),然後 s2 = stringtable[i] ,因此s1、s2 和stringtable[i]都是指向”aaabbbb“。

s3 一開始就是和常量池中的stringtable[j] 相等,由於”ccc“ 已經被stringtable[j]維護,s4.intern(),僅僅返回stringtable[j],但是這個stringtable[j]並沒有再次賦給s4,因此 s3 和 stringtable[j] 指向”ccc“ 而 s4還是指向 另外一個”ccc“。

 

注意:

上面的解釋是基於jdk7/8。jdk1.6不一樣,jdk1.6的intern會在永久代new一個新對象。所以上面案例的在1.6中全部都是false

總結

 

對於可能存在大量的重複字符串,使用intern可以節省內存空間

public class StingIntern {
    static final int MAX_COUNT = 1000 * 10000;
    static final String[] arr = new String[MAX_COUNT];
    public static void main(String[] args) throws InterruptedException {
        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] = String.valueOf(data[i % data.length]);
            //arr[i] = String.valueOf(data[i % data.length]).intern();
        }
        long end = System.currentTimeMillis();
        System.out.println("花費時間:" + (end - start));
        Thread.sleep(1000000000L);
    }
}

 

StringTable的垃圾回收

雖然使用intern可以去重複,節省空姐,但是,String對象還是會創建,創建之後通過equals比較比較之後,將常量池中的引用地址返回,最後本次創建的String如果無引用指向就會被GC。

G1的String去重

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

動機

當前,許多大型Java應用程序已成爲內存瓶頸。測量表明,在這些類型的應用程序中,大約25%的Java堆活動數據集被String對象佔用。此外,這些String對象中大約有一半是重複項,其中重複項 string1.equals(string2)是正確的。String從本質上講,在堆上具有重複的對象只是浪費內存。該項目將String在G1垃圾收集器中實現自動和連續重複數據刪除,以避免浪費內存並減少內存佔用。

實現

當垃圾收集器工作的時候,會訪問堆上存活的對象。對每一個訪問的對象都會檢查是否是候選(達到一定年齡)要去重的String對象。

如果是,把這個對象的一個引用插入到對象等待後續處理。一個去重線程在後臺運行,處理這個隊列的一個元素意味着從隊列刪除這個元素,然後嘗試重用它引用的String對象。

使用一個hashtable來記錄所有的被String對象使用的不重複的char數組,當去重的時候會查這個hashtable,來看堆上是否已經存在一個一模一樣的char數組。

如果存在,String對象會被調整引用那個數組,釋放對原來數組的引用,最後被垃圾回收器回收。

如果查找失敗,char數組會被插入到hashtable。這樣以後就可以共享這個數組了。

 

命令行選項:

  • -XX:+UseStringDeduplication 開啓String去重,默認不開啓,需要手動開啓
  • -XX:+PrintStringDeduplicationStatics 打印詳細去重統計信息
  • -XX:StringDeduplicationAgeThreshold 達到這個年齡的String對象被認爲是去重的候選對象。

 

JVM對String的優化:

  • char 改爲byte。
  • 對重複String進行去重。

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章