String物件和String常量池
1. String的基本特性
- String:字串,使用一對 “” 引起來表示
String s1 = "mogublog" ; // 字面量的定義方式
String s2 = new String("moxi"); // new 物件的方式
- String宣告為final的,不可被繼承 String實現了Serializable介面:表示字串是支援序列化的。實現了Comparable介面:表示String可以比較大小
- string在jdk8及以前內部定義了final char[] value用於儲存字串資料。JDK9時改為byte[]
1.1 為什麼String在jdk9 之後改變了其底層結構
官方解釋: http://openjdk.java.net/jeps/254
-
String類的jdk8之前的實現將字元儲存在char陣列中,每個字元使用兩個位元組(16位)。
-
但是從許多不同的應用程式收集的資料表明,字串是堆使用的主要組成部分,而且大多數字符串物件只包含拉丁字元。這些字元只需要一個位元組的儲存空間,因此這些字串物件的內部char陣列中有一半的空間將不會使用。 例如 存
ab
這個字元,如果使用char 陣列,就要分配兩個字元的空間,即 四個位元組, 但是ab 作為英文,本身只需佔用兩個位元組即可 -
之前 String 類使用 UTF-16 的 char[] 陣列儲存,現在改為 byte[] 陣列 外加一個編碼標誌位儲存,該編碼標誌將指定 String 類中 byte[] 陣列的編碼方式
結論:String再也不用char[] 來儲存了,改成了byte [] 加上編碼標記,節約了一些空間 ,
同時基於String的資料結構,例如StringBuffer和StringBuilder也同樣做了修改
1.2 String 的不可變性
- 當對字串變數重新賦值時,會直接新建一個字串(或池中本來就有的),不會影響本來的字串
- 當對現有的字串進行連線拼接操作時,也需要重新指定記憶體區域賦值,不能使用原有的value進行賦值。
- 當呼叫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 記憶體分配的演進過程
- Java 6及以前,字串常量池存放在永久代
- Java 7中 Oracle的工程師對字串池的邏輯做了很大的改變,即將字串常量池的位置調整到Java堆內
- 所有的字串都儲存在堆(Heap)中,和其他普通物件一樣,這樣可以讓你在進行調優應用時僅需要調整堆大小就可以了。
- 字串常量池概念原本使用得比較多,但是這個改動使得我們有足夠的理由讓我們重新考慮在Java 7中使用String.intern()
JDK6 :
JDK7:
為什麼要調整String 常量池的位置呢
- 永久代的預設比較小
- 永久代垃圾回收頻率低
- 堆中空間足夠大,字串可被及時回收
3 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
字串常量池中同一個字串只存在一份
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. 字串的拼接操作
- 常量與常量的拼接結果在常量池,在編譯期就進行運算了
- 常量池中不會存在相同內容的變數
- 拼接前後,只要其中有一個是變數,就會就在堆中
new
一個。不在常量池中(但是也會建立一個在常量池中),變數拼接的原理是StringBuilder - 如果是
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();
-
intern是一個native方法,呼叫的是底層C的方法
-
字串池最初是空的。在呼叫intern方法時,如果池中已經包含了由equals(object)方法確定的與該字串物件相等的字串(也就是值相等),則返回池中的字串。否則,該字串物件值放一個到池中,並返回對該字串物件的引用。
-
也就是說,如果在任意字串上呼叫String.intern方法,那麼其返回結果所指向的那個類例項,必須和直接以字面量形式出現的字串例項完全相同。因此,下列表達式的值必定是true
("a"+"b"+"c").intern()=="abc"
-
通俗點講,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 物件,將在垃圾回收時 被銷燬,減少記憶體,檢視記憶體資料也是如此
結論:
- 對於程式中大量使用存在的字串時,尤其存在很多已經重複的字串時,使用intern()方法能夠節省記憶體空間。
- 大的網站平臺,需要記憶體中儲存大量的字串。比如社交網站,很多人都儲存:北京市、海淀區等資訊。這時候如果字串都呼叫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 去重操作的背景
-
對許多Java應用(有大的也有小的)做的測試得出以下結果:
- 堆存活資料集合裡面String物件佔了25%
- 堆存活資料集合裡面重複的String物件有13.5%
- String物件的平均長度是45
-
許多大規模的Java應用的瓶頸在於記憶體,測試表明,在這些型別的應用裡面,Java堆中存活的資料集合差不多25%是String物件。更進一步,這裡面差不多一半String物件是重複的,重複的意思是說:
-
str1.equals(str2)= true。堆上存在重複的String物件必然是一種記憶體的浪費。這個專案將在G1垃圾收集器中實現自動持續對重複的String物件進行去重,這樣就能避免浪費記憶體。
String 去重的的具體實現
- 當垃圾收集器工作的時候,會訪問堆上存活的物件。對每一個訪問的物件都會檢查是否是候選的要去重的String物件。
- 如果是,把這個物件的一個引用插入到佇列中等待後續的處理。一個去重的執行緒在後臺執行,處理這個佇列。處理佇列的一個元素意味著從佇列刪除這個元素,然後嘗試去重它引用的String物件。
- 使用一個Hashtable來記錄所有的被String物件(堆中建立的 和 常量池中的)使用的不重複的char陣列。當去重的時候,會查這個Hashtable,來看堆上是否已經存在一個一模一樣的char陣列。
- 如果存在,String物件會被調整內部維護的陣列去,引用那個陣列,釋放對原來的陣列的引用,最終會被垃圾收集器回收掉。
- 如果查詢失敗,char陣列會被插入到Hashtable,這樣以後的時候就可以共享這個陣列了。
命令列選項:
- UseStringDeduplication(bool) :開啟String去重,預設是不開啟的,需要手動開啟。
- PrintStringDeduplicationStatistics(bool) :列印詳細的去重統計資訊
- stringDeduplicationAgeThreshold(uintx) :達到這個年齡的String物件被認為是去重的候選物件