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 [] 存儲?
- String 類的目前實作将字元存儲在 char 數組中,每個字元使用兩個位元組(16位)。從許多不同的應用程式收集的資料表明,字元串是堆使用的主要組成部分,而且大多數字元串對象隻包含拉丁字元(Latin-1),這些字元隻需要一個位元組的存儲空間,是以這些字元串對象的内部 char 數組中有一半的空間将不會使用,産生了大量浪費;
- 之前 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
位元組碼指令:
- 取字元串 “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。 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();
}
}
}
}
}
- -XX:StringTableSize=1009 :程式耗時 505ms.
- -XX:StringTableSize=100009 :程式耗時 116ms.
String 的記憶體配置設定
- 在 Java 語言中有8種基本資料類型和一種比較特殊的類型 String 。這些類型為了使它們在運作過程中速度更快、更節省記憶體,都提供了一種常量池的概念。
- 常量池就類似一個 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() 方法,根據該字元串是否在常量池中存在,分為:
- 如果存在,則傳回字元串在常量池中的位址;
- 如果字元串常量池中不存在該字元串,則在常量池中建立一份,并傳回此對象的位址。
- 常量與常量的拼接結果在常量池,原理是編譯期優化。
示例代碼:
@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
- 拼接前後,隻要其中有一個是變量,結果就在堆中。而調用 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);
}
- 體會執行效率:通過 StringBuilder 的 append() 的方式添加字元串的效率要遠高于使用 String 的字元串拼接方式!
- 原因:
- StringBuilder 的 append() 的方式:自始至終中隻建立過一個 StringBuilder 的對象;
- 使用 String 的字元串拼接方式:建立過多個 StringBuilder 和 String(調的 toString 方法)的對象,記憶體占用更大;如果進行 GC ,需要花費額外的時間(在拼接的過程中産生的一些中間字元串可能永遠也用不到,會産生大量垃圾字元串)。
- 改進的空間:
- 在實際開發中,如果基本确定要前前後後添加的字元串長度不高于某個限定值 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
位元組碼指令分析:
- 0 new #2 <java/lang/StringBuilder> :拼接字元串會建立一個 StringBuilder 對象;
- 7 new #4 <java/lang/String> :建立 String 對象,對應于 new String(“a”);
- 11 ldc #5 :在字元串常量池中放入 “a”(如果之前字元串常量池中沒有 “a” 的話);
- 19 new #4 <java/lang/String> :建立 String 對象,對應于 new String(“b”);
- 23 ldc #8 :在字元串常量池中放入 “b”(如果之前字元串常量池中沒有 “b” 的話);
- 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() 則把字元串放入常量池中。
JDK7 之後:
- 對 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:
JDK7/8:
練習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
}
}
練習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去重操作的背景
注意不是字元串常量池的去重操作,字元串常量池本身就沒有重複的。
- 背景:對許多 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 對象被認為是去重的候選對象。